diff --git a/app/blueprints/packages/__init__.py b/app/blueprints/packages/__init__.py
index aab0512e..3335abae 100644
--- a/app/blueprints/packages/__init__.py
+++ b/app/blueprints/packages/__init__.py
@@ -84,4 +84,4 @@ def get_package_tabs(user: User, package: Package):
return retval
-from . import packages, screenshots, releases, reviews, game_hub
+from . import packages, advanced_search, screenshots, releases, reviews, game_hub
diff --git a/app/blueprints/packages/advanced_search.py b/app/blueprints/packages/advanced_search.py
new file mode 100644
index 00000000..75246857
--- /dev/null
+++ b/app/blueprints/packages/advanced_search.py
@@ -0,0 +1,92 @@
+# ContentDB
+# Copyright (C) 2024 rubenwardy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from flask import render_template
+from flask_babel import lazy_gettext, gettext
+from flask_wtf import FlaskForm
+from wtforms.fields.choices import SelectField, SelectMultipleField
+from wtforms.fields.simple import StringField, BooleanField
+from wtforms.validators import Optional
+from wtforms_sqlalchemy.fields import QuerySelectMultipleField, QuerySelectField
+
+from . import bp
+from ...models import PackageType, Tag, db, ContentWarning, License, Language, MinetestRelease
+
+
+def make_label(obj: Tag | ContentWarning):
+ translated = obj.get_translated()
+ if translated["description"]:
+ return "{}: {}".format(translated["title"], translated["description"])
+ else:
+ return translated["title"]
+
+
+def get_hide_choices():
+ ret = [
+ ("android_default", gettext("Android Default")),
+ ("desktop_default", gettext("Desktop Default")),
+ ("nonfree", gettext("Non-free")),
+ ("wip", gettext("Work in Progress")),
+ ("deprecated", gettext("Deprecated")),
+ ("*", gettext("All content warnings")),
+ ]
+ content_warnings = ContentWarning.query.order_by(db.asc(ContentWarning.name)).all()
+ tags = Tag.query.order_by(db.asc(Tag.name)).all()
+ ret += [(x.name, make_label(x)) for x in content_warnings + tags]
+ return ret
+
+
+class AdvancedSearchForm(FlaskForm):
+ q = StringField(lazy_gettext("Query"), [Optional()])
+ type = SelectMultipleField(lazy_gettext("Type"), [Optional()], choices=PackageType.choices(),
+ coerce=PackageType.coerce)
+ author = StringField(lazy_gettext("Author"), [Optional()])
+ tag = QuerySelectMultipleField(lazy_gettext('Tags'),
+ query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)),
+ get_pk=lambda a: a.id, get_label=make_label)
+ flag = QuerySelectMultipleField(lazy_gettext('Content Warnings'), query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=make_label)
+ license = QuerySelectMultipleField(lazy_gettext("License"), [Optional()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ game = StringField(lazy_gettext("Supports Game"), [Optional()])
+ lang = QuerySelectField(lazy_gettext("Language"), allow_blank=True,
+ query_factory=lambda: Language.query.order_by(db.asc(Language.title)),
+ get_pk=lambda a: a.id, get_label=lambda a: a.title)
+ hide = SelectMultipleField(lazy_gettext("Hide Tags and Content Warnings"), [Optional()])
+ engine_version = QuerySelectField(lazy_gettext("Minetest Version"), allow_blank=True,
+ query_factory=lambda: MinetestRelease.query.order_by(db.asc(MinetestRelease.id)),
+ get_pk=lambda a: a.value, get_label=lambda a: a.name)
+ sort = SelectField(lazy_gettext("Sort by"), [Optional()], choices=[
+ ("", ""),
+ ("name", lazy_gettext("Name")),
+ ("title", lazy_gettext("Title")),
+ ("score", lazy_gettext("Package score")),
+ ("reviews", lazy_gettext("Reviews")),
+ ("downloads", lazy_gettext("Downloads")),
+ ("created_at", lazy_gettext("Created At")),
+ ("approved_at", lazy_gettext("Approved At")),
+ ("last_release", lazy_gettext("Last Release")),
+ ])
+ order = SelectField(lazy_gettext("Order"), [Optional()], choices=[
+ ("desc", lazy_gettext("Descending")),
+ ("asc", lazy_gettext("Ascending")),
+ ])
+ random = BooleanField(lazy_gettext("Random order"))
+
+
+@bp.route("/packages/advanced-search/")
+def advanced_search():
+ form = AdvancedSearchForm()
+ form.hide.choices = get_hide_choices()
+ return render_template("packages/advanced_search.html", form=form)
diff --git a/app/models/packages.py b/app/models/packages.py
index 81f37871..e12d1d99 100644
--- a/app/models/packages.py
+++ b/app/models/packages.py
@@ -126,7 +126,7 @@ class PackageType(enum.Enum):
@classmethod
def choices(cls):
- return [(choice, choice.text) for choice in cls]
+ return [(choice.name.lower(), choice.text) for choice in cls]
@classmethod
def coerce(cls, item):
@@ -853,7 +853,7 @@ class Package(db.Model):
}
def recalculate_score(self):
- review_scores = [ 100 * r.as_weight() for r in self.reviews ]
+ review_scores = [ 150 * r.as_weight() for r in self.reviews ]
self.score = self.score_downloads + sum(review_scores)
def get_conf_file_name(self):
@@ -1042,6 +1042,10 @@ class MinetestRelease(db.Model):
self.name = name
self.protocol = protocol
+ @property
+ def value(self):
+ return self.name
+
def get_actual(self):
return None if self.name == "None" else self
diff --git a/app/querybuilder.py b/app/querybuilder.py
index 52dd559d..a02ce101 100644
--- a/app/querybuilder.py
+++ b/app/querybuilder.py
@@ -138,6 +138,8 @@ class QueryBuilder:
self.lucky = "lucky" in args
self.limit = 1 if self.lucky else get_int_or_abort(args.get("limit"), None)
self.order_by = args.get("sort")
+ if self.order_by == "":
+ self.order_by = None
self.order_dir = args.get("order") or "desc"
if "android_default" in self.hide_flags:
@@ -161,6 +163,9 @@ class QueryBuilder:
protocol_version = get_int_or_abort(args.get("protocol_version"))
minetest_version = args.get("engine_version")
+ if minetest_version == "__None":
+ minetest_version = None
+
if protocol_version or minetest_version:
self.version = MinetestRelease.get(minetest_version, protocol_version)
else:
@@ -176,8 +181,14 @@ class QueryBuilder:
self.game = args.get("game")
if self.game:
self.game = Package.get_by_key(self.game)
+ if self.game is None:
+ abort(make_response("Unable to find that game"), 400)
+ else:
+ self.game = None
self.has_lang = args.get("lang")
+ if self.has_lang == "__None":
+ self.has_lang = None
if cookies and request.cookies.get("hide_nonfree") == "1":
self.hide_nonfree = True
@@ -244,7 +255,7 @@ class QueryBuilder:
if self.game:
query = query.filter(Package.supported_games.any(game=self.game, supports=True))
- if self.has_lang:
+ if self.has_lang and self.has_lang != "en":
query = query.filter(Package.translations.any(language_id=self.has_lang))
for tag in self.tags:
diff --git a/app/templates/base.html b/app/templates/base.html
index 2256f8b6..487c4274 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -253,6 +253,7 @@
{% if request.endpoint != "flatpage" and request.endpoint != "report.report" %}
{{ _("Report / DMCA") }}
{% endif %}
+ {{ _("Advanced Search") }}
{{ _("User List") }}
{{ _("Threads") }}
{{ _("Collections") }}
diff --git a/app/templates/packages/advanced_search.html b/app/templates/packages/advanced_search.html
new file mode 100644
index 00000000..2f1a7697
--- /dev/null
+++ b/app/templates/packages/advanced_search.html
@@ -0,0 +1,36 @@
+{% extends "base.html" %}
+
+{% block title %}
+{{ _("Advanced Search") }}
+{% endblock %}
+
+{% block content %}
+ {{ self.title() }}
+
+ {% from "macros/forms.html" import render_field, render_checkbox_field, render_submit_field %}
+
+{% endblock %}