From c498818e8b35cdb8121eaa35505b3f6188b295c3 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Sun, 18 Jun 2023 21:11:17 +0100 Subject: [PATCH] Add supports_all_games to make game support explicit Fixes #388 and fixes #441 --- app/blueprints/admin/actions.py | 35 +++++++++-- app/blueprints/api/endpoints.py | 2 +- app/blueprints/packages/packages.py | 24 ++++++-- app/blueprints/todo/user.py | 26 +++++++- app/flatpages/help/game_support.md | 12 +++- app/models/packages.py | 21 +++++-- app/tasks/importtasks.py | 6 +- app/templates/packages/game_support.html | 21 +++++-- app/templates/packages/view.html | 75 +++++++++++++++++++----- app/templates/todo/game_support.html | 28 +++++++-- migrations/versions/2ecff2f9972d_.py | 26 ++++++++ 11 files changed, 232 insertions(+), 44 deletions(-) create mode 100644 migrations/versions/2ecff2f9972d_.py diff --git a/app/blueprints/admin/actions.py b/app/blueprints/admin/actions.py index 8b4cdb09..e78345a0 100644 --- a/app/blueprints/admin/actions.py +++ b/app/blueprints/admin/actions.py @@ -140,8 +140,6 @@ def remind_wip(): packages = [pkg[0] for pkg in packages] packages_list = _package_list(packages) havent = "haven't" if len(packages) > 1 else "hasn't" - if len(packages_list) + 54 > 100: - packages_list = packages_list[0:(100-54-1)] + "…" addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL, f"Did you forget? {packages_list} {havent} been submitted for review yet", @@ -156,7 +154,7 @@ def remind_outdated(): system_user = get_system_user() for user in users: packages = db.session.query(Package.title).filter( - Package.maintainers.any(User.id==user.id), + Package.maintainers.contains(user), Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \ .all() @@ -234,7 +232,7 @@ def remind_video_url(): system_user = get_system_user() for user in users: packages = db.session.query(Package.title).filter( - or_(Package.author==user, Package.maintainers.any(User.id==user.id)), + or_(Package.author==user, Package.maintainers.contains(user)), Package.video_url==None, Package.type == PackageType.GAME, Package.state == PackageState.APPROVED) \ @@ -250,6 +248,35 @@ def remind_video_url(): db.session.commit() +@action("Send missing game support notifications") +def remind_missing_game_support(): + users = User.query.filter( + User.maintained_packages.any(and_( + Package.state != PackageState.DELETED, + Package.type.in_([PackageType.MOD, PackageType.TXP]), + ~Package.supported_games.any(), + Package.supports_all_games == False))).all() + + system_user = get_system_user() + for user in users: + packages = db.session.query(Package.title).filter( + Package.maintainers.contains(user), + Package.state != PackageState.DELETED, + Package.type.in_([PackageType.MOD, PackageType.TXP]), + ~Package.supported_games.any(), + Package.supports_all_games == False) \ + .all() + + packages = [pkg[0] for pkg in packages] + packages_list = _package_list(packages) + + addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL, + f"You need to confirm whether the following packages support all games: {packages_list}", + url_for('todo.all_game_support', username=user.username)) + + db.session.commit() + + @action("Detect game support") def detect_game_support(): task_id = uuid() diff --git a/app/blueprints/api/endpoints.py b/app/blueprints/api/endpoints.py index d38149a8..02849485 100644 --- a/app/blueprints/api/endpoints.py +++ b/app/blueprints/api/endpoints.py @@ -230,7 +230,7 @@ def list_all_releases(): if maintainer is None: error(404, "Maintainer not found") query = query.join(Package) - query = query.filter(Package.maintainers.any(id=maintainer.id)) + query = query.filter(Package.maintainers.contains(maintainer)) return jsonify([ rel.getLongAsDictionary() for rel in query.limit(30).all() ]) diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index 467d6931..d9b1586b 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -644,6 +644,7 @@ class GameSupportForm(FlaskForm): enable_support_detection = BooleanField(lazy_gettext("Enable support detection based on dependencies (recommended)"), [Optional()]) supported = StringField(lazy_gettext("Supported games (Comma-separated)"), [Optional()]) unsupported = StringField(lazy_gettext("Unsupported games (Comma-separated)"), [Optional()]) + supports_all_games = BooleanField(lazy_gettext("Supports all games (unless stated)"), [Optional()]) submit = SubmitField(lazy_gettext("Save")) @@ -661,17 +662,25 @@ def game_support(package): force_game_detection = package.supported_games.filter(and_( PackageGameSupport.confidence > 1, PackageGameSupport.supports == True)).count() == 0 + can_override = can_edit and current_user not in package.maintainers + form = GameSupportForm() if can_edit else None if form and request.method == "GET": form.enable_support_detection.data = package.enable_game_support_detection - manual_supported_games = package.supported_games.filter_by(confidence=11).all() - form.supported.data = ", ".join([x.game.name for x in manual_supported_games if x.supports]) - form.unsupported.data = ", ".join([x.game.name for x in manual_supported_games if not x.supports]) + form.supports_all_games.data = package.supports_all_games + + if can_override: + manual_supported_games = package.supported_games.filter_by(confidence=11).all() + form.supported.data = ", ".join([x.game.name for x in manual_supported_games if x.supports]) + form.unsupported.data = ", ".join([x.game.name for x in manual_supported_games if not x.supports]) + else: + form.supported = None + form.unsupported = None if form and form.validate_on_submit(): detect_update_needed = False - if current_user not in package.maintainers: + if can_override: try: resolver = GameSupportResolver(db.session) @@ -695,6 +704,8 @@ def game_support(package): else: package.supported_games.filter_by(confidence=1).delete() + package.supports_all_games = form.supports_all_games.data + db.session.commit() if detect_update_needed: @@ -708,7 +719,10 @@ def game_support(package): all_game_support = package.supported_games.all() all_game_support.sort(key=lambda x: -x.game.score) - supported_games = ", ".join([x.game.name for x in all_game_support if x.supports]) + supported_games_list: List[str] = [x.game.name for x in all_game_support if x.supports] + if package.supports_all_games: + supported_games_list.insert(0, "*") + supported_games = ", ".join(supported_games_list) unsupported_games = ", ".join([x.game.name for x in all_game_support if not x.supports]) mod_conf_lines = "" diff --git a/app/blueprints/todo/user.py b/app/blueprints/todo/user.py index 104d7389..728f2c81 100644 --- a/app/blueprints/todo/user.py +++ b/app/blueprints/todo/user.py @@ -15,7 +15,8 @@ # along with this program. If not, see . from celery import uuid -from flask import redirect, url_for, abort, render_template +from flask import redirect, url_for, abort, render_template, flash +from flask_babel import gettext from flask_login import current_user, login_required from sqlalchemy import or_, and_ @@ -23,7 +24,6 @@ from app.models import User, Package, PackageState, PackageScreenshot, PackageUp PackageRelease, Permission, NotificationType, AuditSeverity, UserRank, PackageType from app.tasks.importtasks import makeVCSRelease from app.utils import addNotification, addAuditLog - from . import bp @@ -59,7 +59,8 @@ def view_user(username=None): missing_game_support = user.maintained_packages.filter( Package.state != PackageState.DELETED, Package.type.in_([PackageType.MOD, PackageType.TXP]), - ~Package.supported_games.any()) \ + ~Package.supported_games.any(), + Package.supports_all_games == False) \ .order_by(db.asc(Package.title)).all() packages_with_no_screenshots = user.maintained_packages.filter( @@ -152,3 +153,22 @@ def all_game_support(username=None): .order_by(db.asc(Package.title)).all() return render_template("todo/game_support.html", user=user, packages=packages) + + +@bp.route("/users//confirm_supports_all_games/", methods=["POST"]) +@login_required +def confirm_supports_all_games(username=None): + user: User = User.query.filter_by(username=username).one_or_404() + if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR): + abort(403) + + db.session.query(Package).filter( + Package.maintainers.contains(user), + Package.state != PackageState.DELETED, + Package.type.in_([PackageType.MOD, PackageType.TXP]), + ~Package.supported_games.any(supports=True)).update({ "supports_all_games": True }) + + db.session.commit() + + flash(gettext("Done"), "success") + return redirect(url_for("todo.all_game_support", username=current_user.username)) diff --git a/app/flatpages/help/game_support.md b/app/flatpages/help/game_support.md index d38feae8..da6c6af6 100644 --- a/app/flatpages/help/game_support.md +++ b/app/flatpages/help/game_support.md @@ -12,7 +12,7 @@ user experience. ## Support sources -### mod.conf +### mod.conf / texture_pack.conf You can use `supported_games` to specify games that your mod is compatible with. @@ -24,6 +24,16 @@ Both of these are comma-separated lists of game technical ids. Any `_game` suffi supported_games = minetest_game, repixture unsupported_games = lordofthetest, nodecore, whynot +If your package supports all games by default, you can put "*` in supported_games. +You can still use unsupported_games to mark games as unsupported. +You can also specify games that you've tested in supported_games. + + # Should work with all games but I've only tested using Minetest Game: + supported_games = *, minetest_games + + # But doesn't work in capturetheflag + unsupported_game = capturetheflag + ### Dependencies ContentDB will analyse hard dependencies and work out which games a mod supports. diff --git a/app/models/packages.py b/app/models/packages.py index 5f35d43d..b155fb95 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -17,6 +17,7 @@ import datetime import enum +import typing from flask import url_for from flask_babel import lazy_gettext @@ -410,6 +411,9 @@ class Package(db.Model): review_thread = db.relationship("Thread", uselist=False, foreign_keys=[review_thread_id], back_populates="is_review_thread", post_update=True) + # Supports all games by default, may have unsupported games + supports_all_games = db.Column(db.Boolean, nullable=False, default=False) + # Downloads repo = db.Column(db.String(200), nullable=True) website = db.Column(db.String(200), nullable=True) @@ -520,15 +524,24 @@ class Package(db.Model): def getSortedOptionalDependencies(self): return self.getSortedDependencies(False) - def getSortedSupportedGames(self, include_unsupported=False): - query = self.supported_games - if not include_unsupported: - query = query.filter(PackageGameSupport.game.has(state=PackageState.APPROVED)).filter_by(supports=True) + def get_sorted_game_support(self): + query = self.supported_games.filter(PackageGameSupport.game.has(state=PackageState.APPROVED)) supported = query.all() supported.sort(key=lambda x: -(x.game.score + 100000*x.confidence)) return supported + def get_sorted_game_support_pair(self): + supported = self.get_sorted_game_support() + return [ + [x for x in supported if x.supports], + [x for x in supported if not x.supports], + ] + + def has_game_support_confirmed(self): + return self.supports_all_games or \ + self.supported_games.filter(PackageGameSupport.confidence > 1).count() > 0 + def getAsDictionaryKey(self): return { "name": self.name, diff --git a/app/tasks/importtasks.py b/app/tasks/importtasks.py index 8412f723..4f282f42 100644 --- a/app/tasks/importtasks.py +++ b/app/tasks/importtasks.py @@ -80,7 +80,7 @@ def getMeta(urlstr, author): @celery.task() def updateAllGameSupport(): resolver = GameSupportResolver(db.session) - resolver.update_all() + resolver.init_all() db.session.commit() @@ -153,6 +153,10 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path): if "supported_games" in tree.meta: for game in get_games_from_csv(db.session, tree.meta["supported_games"]): game_is_supported[game.id] = True + + has_star = any(map(lambda x: x.strip() == "*", tree.meta["supported_games"].split(","))) + if has_star: + package.supports_all_games = True if "unsupported_games" in tree.meta: for game in get_games_from_csv(db.session, tree.meta["unsupported_games"]): game_is_supported[game.id] = False diff --git a/app/templates/packages/game_support.html b/app/templates/packages/game_support.html index 98cd9fb9..c5d6cb5f 100644 --- a/app/templates/packages/game_support.html +++ b/app/templates/packages/game_support.html @@ -9,9 +9,8 @@ {{ _("Documentation") }}

{{ self.title() }}

- -

- This feature is experimental +

+ {{ _("Game support is configured using the package's .conf file. See the documentation for more info") }}

@@ -29,7 +28,13 @@
- {% for support in package.getSortedSupportedGames(True) %} + {% if package.supports_all_games %} +
+ {{ _("Supports all games unless otherwise stated") }} +
+ {% endif %} + + {% for support in package.get_sorted_game_support() %}
@@ -83,7 +88,13 @@

{% endif %} - {% if package.checkPerm(current_user, "EDIT_PACKAGE") and current_user not in package.maintainers %} + {{ render_checkbox_field(form.supports_all_games) }} +

+ {{ _("Unless otherwise stated, this package should work with all games.") }} + {{ _("You can check this and still specify games in supported_games that you've tested.") }} +

+ + {% if form.supported and form.unsupported %}

{{ _("Editor Overrides") }} diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html index 6b338771..c211dfa4 100644 --- a/app/templates/packages/view.html +++ b/app/templates/packages/view.html @@ -445,8 +445,11 @@ {% endif %} {% if package.type == package.type.MOD or package.type == package.type.TXP %} - {% set supported_games = package.getSortedSupportedGames() %} - {% if supported_games or package.type == package.type.MOD %} + {% set pair = package.get_sorted_game_support_pair() %} + {% set supported_games = pair[0] %} + {% set unsupported_games = pair[1] %} + {% set show_unsupported = package.supports_all_games or supported_games == [] %} + {% if supported_games or unsupported_games or package.type == package.type.MOD %}

{% if package.checkPerm(current_user, "EDIT_PACKAGE") %} @@ -455,19 +458,63 @@ {% endif %} {{ _("Compatible Games") }}

-
- {% for support in supported_games %} - - {{ _("%(title)s by %(display_name)s", - title=support.game.title, display_name=support.game.author.display_name) }} - - {% else %} - {{ _("No specific game is required") }} - {% endfor %} -
- {% if package.type == package.type.MOD %} + {% if package.supports_all_games %} +

+ {{ _("Should support most games.") }} + {% if supported_games %} +
+ {{ _("Tested with:") }} + {% endif %} +

+ {% endif %} + + {% if supported_games %} + + {% elif not package.supports_all_games %} +

+ {{ _("No specific game required") }} +

+ {% if package.checkPerm(current_user, "EDIT_PACKAGE") %} +
+

+ {{ _("Is the above correct?") }} + {{ _("You need to either confirm this or tell ContentDB about supported games") }} +

+ + + Update + +
+ {% endif %} + {% endif %} + + {% if unsupported_games and show_unsupported %} +

+ {{ _("Except for:") }} +

+
+ {% for support in unsupported_games %} + + + {{ _("%(title)s by %(display_name)s", + title=support.game.title, display_name=support.game.author.display_name) }} + + {% endfor %} +
+ {% endif %} + + {% if package.type == package.type.MOD and (supported_games or unsupported_games) and + not package.has_game_support_confirmed() %}

{{ _("This is an experimental feature.") }} {{ _("Supported games are determined by an algorithm, and may not be correct.") }} diff --git a/app/templates/todo/game_support.html b/app/templates/todo/game_support.html index f49710f8..6d5a454c 100644 --- a/app/templates/todo/game_support.html +++ b/app/templates/todo/game_support.html @@ -10,6 +10,7 @@

{{ _("You should specify the games supported by your mods and texture packs.") }} {{ _("Specifying game support makes it easier for players to find your content.") }} + {{ _("If your package supports all games unless otherwise stated, confirm this using 'Supports all games'") }}

@@ -28,7 +29,15 @@
- {% set supported_games = package.getSortedSupportedGames() %} + {% if package.supports_all_games %} + + + {{ _("Supports all games") }} + + + {% endif %} + + {% set supported_games = package.get_sorted_game_support_pair()[0] %} {% if supported_games %} {% for support in supported_games %} {% endfor %} - {% else %} - - - {{ _("None listed, assumed to support all games") }} - + {% elif not package.supports_all_games %} + + {{ _("No supported games listed. Please either add supported games or check 'Supports all games'") }} {% endif %}
@@ -55,4 +62,13 @@

{{ _("Nothing to do :)") }}

{% endfor %} + +

Bulk support all games

+

+ {{ _("Click the button below to confirm that all games without listed supported_games (red text above) do support all games, except for any games listed in unsupported_games.") }} +

+
+ + +
{% endblock %} diff --git a/migrations/versions/2ecff2f9972d_.py b/migrations/versions/2ecff2f9972d_.py new file mode 100644 index 00000000..b0f4d79a --- /dev/null +++ b/migrations/versions/2ecff2f9972d_.py @@ -0,0 +1,26 @@ +"""empty message + +Revision ID: 2ecff2f9972d +Revises: 23afcf580aae +Create Date: 2023-06-18 07:51:42.581955 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '2ecff2f9972d' +down_revision = '23afcf580aae' +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table('package', schema=None) as batch_op: + batch_op.add_column(sa.Column('supports_all_games', sa.Boolean(), nullable=False, server_default="false")) + + +def downgrade(): + with op.batch_alter_table('package', schema=None) as batch_op: + batch_op.drop_column('supports_all_games')