From 21fe900cc8ca797f17cfed55cfe531a9ffc74b5d Mon Sep 17 00:00:00 2001 From: Armen Date: Tue, 15 Mar 2022 20:55:39 -0400 Subject: [PATCH] tag-based navigation implemented, added in-line screenshot viewer, fixes #10 --- app/blueprints/admin/actions.py | 23 ++- app/blueprints/api/endpoints.py | 45 ++---- app/blueprints/homepage/__init__.py | 12 +- app/blueprints/packages/game_hub.py | 17 +- app/blueprints/packages/packages.py | 52 ++++--- app/blueprints/packages/releases.py | 14 +- app/blueprints/packages/reviews.py | 8 +- app/blueprints/packages/screenshots.py | 6 +- app/blueprints/users/account.py | 17 +- app/blueprints/users/profile.py | 19 +-- app/default_data.py | 48 +++--- app/flatpages/help/api.md | 5 +- app/flatpages/help/faq.md | 2 +- app/logic/game_support.py | 49 +++--- app/logic/packages.py | 16 +- app/models/__init__.py | 2 +- app/models/packages.py | 133 +++++----------- app/public/static/lg-logo.png | Bin 0 -> 16601 bytes app/querybuilder.py | 22 +-- app/tasks/appstreamtasks.py | 56 +++++-- app/tasks/forumtasks.py | 118 +++++++------- app/tasks/importtasks.py | 9 +- app/templates/base.html | 23 ++- app/templates/emails/notification.html | 2 +- app/templates/index.html | 30 +--- app/templates/macros/package_approval.html | 2 +- app/templates/macros/packagegridtile.html | 4 +- app/templates/macros/reviews.html | 4 +- app/templates/packages/create_edit.html | 2 +- app/templates/packages/game_hub.html | 10 +- app/templates/packages/list.html | 2 +- app/templates/packages/release_wizard.html | 3 +- .../packages/review_create_edit.html | 2 +- app/templates/packages/view.html | 147 +++++++++--------- app/templates/users/login.html | 2 +- app/templates/users/register.html | 2 +- app/tests/integ/test_releases_queries.py | 34 ++-- app/utils/__init__.py | 3 + app/utils/lists.py | 2 +- app/utils/models.py | 4 +- migrations/versions/668167a0e2d8_.py | 34 +++- requirements.lock.txt | 1 + requirements.txt | 1 + translations/de/LC_MESSAGES/messages.po | 28 ++-- translations/es/LC_MESSAGES/messages.po | 24 +-- translations/fr/LC_MESSAGES/messages.po | 28 ++-- translations/hu/LC_MESSAGES/messages.po | 14 +- translations/id/LC_MESSAGES/messages.po | 28 ++-- translations/lzh/LC_MESSAGES/messages.po | 14 +- translations/messages.pot | 14 +- translations/ms/LC_MESSAGES/messages.po | 28 ++-- translations/nb_NO/LC_MESSAGES/messages.po | 14 +- translations/ru/LC_MESSAGES/messages.po | 28 ++-- translations/tr/LC_MESSAGES/messages.po | 14 +- translations/uk/LC_MESSAGES/messages.po | 14 +- translations/zh_Hans/LC_MESSAGES/messages.po | 20 +-- translations/zh_Hant/LC_MESSAGES/messages.po | 14 +- 57 files changed, 624 insertions(+), 645 deletions(-) create mode 100644 app/public/static/lg-logo.png diff --git a/app/blueprints/admin/actions.py b/app/blueprints/admin/actions.py index 68762b9..8c62a18 100644 --- a/app/blueprints/admin/actions.py +++ b/app/blueprints/admin/actions.py @@ -26,8 +26,8 @@ from sqlalchemy import or_, and_ from app.logic.game_support import GameSupportResolver from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \ - NotificationType, PackageUpdateConfig, License, UserRank, PackageType, PackageGameSupport -from app.tasks.forumtasks import importTopicList, checkAllForumAccounts + NotificationType, PackageUpdateConfig, License, UserRank, PackageGameSupport +# from app.tasks.forumtasks import importTopicList, checkAllForumAccounts from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates from app.tasks.appstreamtasks import importFromFlathub from app.utils import addNotification, get_system_user @@ -90,20 +90,20 @@ def reimport_packages(): return redirect(url_for("todo.view_editor")) -@action("Import forum topic list") -def import_topic_list(): - task = importTopicList.delay() - return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page"))) +# @action("Import forum topic list") +# def import_topic_list(): +# task = importTopicList.delay() +# return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page"))) @action("Import appstream from flathub") def import_from_flathub(): task = importFromFlathub.delay() return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics"))) -@action("Check all forum accounts") -def check_all_forum_accounts(): - task = checkAllForumAccounts.delay() - return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page"))) +# @action("Check all forum accounts") +# def check_all_forum_accounts(): +# task = checkAllForumAccounts.delay() +# return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page"))) @action("Import screenshots") @@ -297,13 +297,12 @@ def delete_inactive_users(): @action("Send Video URL notification") def remind_video_url(): users = User.query.filter(User.maintained_packages.any( - and_(Package.video_url.is_(None), Package.type==PackageType.GAME, Package.state==PackageState.APPROVED))) + and_(Package.video_url.is_(None), Package.state==PackageState.APPROVED))) 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)), Package.video_url.is_(None), - Package.type == PackageType.GAME, Package.state == PackageState.APPROVED) \ .all() diff --git a/app/blueprints/api/endpoints.py b/app/blueprints/api/endpoints.py index 428b3b3..b2d45ae 100644 --- a/app/blueprints/api/endpoints.py +++ b/app/blueprints/api/endpoints.py @@ -25,10 +25,10 @@ from sqlalchemy.sql.expression import func from app import csrf from app.markdown import render_markdown -from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, ForumTopic, \ +from app.models import Tag, PackageState, Package, db, PackageRelease, Permission, ForumTopic, \ APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread from app.querybuilder import QueryBuilder -from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes +from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes, get_toplevel_tags from . import bp from .auth import is_api_authd from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \ @@ -89,9 +89,6 @@ def resolve_package_deps(out, package, only_hard, depth=1): ret = [] out[id] = ret - if package.type != PackageType.TOOL: - return - for dep in package.dependencies: if only_hard and dep.optional: continue @@ -106,7 +103,7 @@ def resolve_package_deps(out, package, only_hard, depth=1): fulfilled_by = [ pkg.getId() for pkg in dep.meta_package.packages] if depth == 1 and not dep.optional: - most_likely = next((pkg for pkg in dep.meta_package.packages if pkg.type == PackageType.TOOL), None) + most_likely = next((pkg for pkg in dep.meta_package.packages), None) if most_likely: resolve_package_deps(out, most_likely, only_hard, depth + 1) @@ -470,9 +467,11 @@ def homepage(): featured = query.filter(Package.tags.any(name="featured")).order_by( func.random()).limit(6).all() new = query.order_by(db.desc(Package.approved_at)).limit(4).all() - pop_mod = query.filter_by(type=PackageType.TOOL).order_by(db.desc(Package.score)).limit(8).all() - pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(8).all() - pop_txp = query.filter_by(type=PackageType.ASSETPACK).order_by(db.desc(Package.score)).limit(8).all() + toplevel_tags = get_toplevel_tags() + popular = {} + # new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all() + for toplevel_tag in toplevel_tags: + popular[toplevel_tag.name] = query.filter(Package.tags.any(name=toplevel_tag.name)).order_by(db.desc(Package.score)).limit(8).all() high_reviewed = query.order_by(db.desc(Package.score - Package.score_downloads)) \ .filter(Package.reviews.any()).limit(4).all() @@ -487,16 +486,15 @@ def homepage(): def mapPackages(packages: List[Package]): return [pkg.getAsDictionaryShort(current_app.config["BASE_URL"]) for pkg in packages] - + popular = { k:mapPackages(v) for (k, v) in popular.items() } return jsonify({ "count": count, "downloads": downloads, "featured": mapPackages(featured), "new": mapPackages(new), "updated": mapPackages(updated), - "pop_mod": mapPackages(pop_mod), - "pop_txp": mapPackages(pop_txp), - "pop_game": mapPackages(pop_gam), + "popular": popular, + "toplevel": [ x.getAsDictionary() for x in toplevel_tags ], "high_reviewed": mapPackages(high_reviewed) }) @@ -505,7 +503,7 @@ def homepage(): @cors_allowed def welcome_v1(): featured = Package.query \ - .filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED, + .filter(Package.state == PackageState.APPROVED, Package.tags.any(name="featured")) \ .order_by(func.random()) \ .limit(5).all() @@ -520,23 +518,6 @@ def welcome_v1(): "featured": map_packages(featured), }) - -# @bp.route("/api/minetest_versions/") -# @cors_allowed -# def versions(): -# protocol_version = request.args.get("protocol_version") -# engine_version = request.args.get("engine_version") -# if protocol_version or engine_version: -# rel = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version)) -# if rel is None: -# error(404, "No releases found") - -# return jsonify(rel.getAsDictionary()) - -# return jsonify([rel.getAsDictionary() \ -# for rel in MinetestRelease.query.all() if rel.getActual() is not None]) - - @bp.route("/api/dependencies/") @cors_allowed def all_deps(): @@ -545,7 +526,7 @@ def all_deps(): def format_pkg(pkg: Package): return { - "type": pkg.type.toName(), + # "type": pkg.type.toName(), "author": pkg.author.username, "name": pkg.name, "provides": [x.name for x in pkg.provides], diff --git a/app/blueprints/homepage/__init__.py b/app/blueprints/homepage/__init__.py index 8a7d9a7..519aca4 100644 --- a/app/blueprints/homepage/__init__.py +++ b/app/blueprints/homepage/__init__.py @@ -3,6 +3,7 @@ from flask import Blueprint, render_template, redirect bp = Blueprint("homepage", __name__) from app.models import * +from app.utils import get_toplevel_tags from sqlalchemy.orm import joinedload from sqlalchemy.sql.expression import func @@ -18,11 +19,12 @@ def home(): count = query.count() featured = query.filter(Package.tags.any(name="featured")).order_by(func.random()).limit(6).all() - + toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all() + popular = {} new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all() - pop_mod = join(query.filter_by(type=PackageType.TOOL).order_by(db.desc(Package.score))).limit(8).all() - pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all() - pop_txp = join(query.filter_by(type=PackageType.ASSETPACK).order_by(db.desc(Package.score))).limit(8).all() + for toplevel_tag in toplevel_tags: + popular[toplevel_tag.name] = join(query.filter(Package.tags.any(name=toplevel_tag.name)).order_by(db.desc(Package.score))).limit(8).all() + high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \ .filter(Package.reviews.any()).limit(4).all() @@ -41,4 +43,4 @@ def home(): .select_from(Tag).outerjoin(Tags).group_by(Tag.id).order_by(db.asc(Tag.title)).all() return render_template("index.html", count=count, downloads=downloads, tags=tags, featured=featured, - new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam, high_reviewed=high_reviewed, reviews=reviews) + new=new, updated=updated, popular=popular, toplevel=toplevel_tags, high_reviewed=high_reviewed, reviews=reviews) diff --git a/app/blueprints/packages/game_hub.py b/app/blueprints/packages/game_hub.py index 2388e7a..a724ed5 100644 --- a/app/blueprints/packages/game_hub.py +++ b/app/blueprints/packages/game_hub.py @@ -18,16 +18,13 @@ from flask import render_template, abort from sqlalchemy.orm import joinedload from . import bp -from app.utils import is_package_page -from ...models import Package, PackageType, PackageState, db, PackageRelease +from app.utils import is_package_page, get_toplevel_tags +from ...models import Package, PackageState, db, PackageRelease @bp.route("/packages///hub/") @is_package_page def game_hub(package: Package): - if package.type != PackageType.GAME: - abort(404) - def join(query): return query.options( joinedload(Package.license), @@ -37,9 +34,11 @@ def game_hub(package: Package): count = query.count() new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all() - pop_mod = join(query.filter_by(type=PackageType.TOOL).order_by(db.desc(Package.score))).limit(8).all() - pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all() - pop_txp = join(query.filter_by(type=PackageType.ASSETPACK).order_by(db.desc(Package.score))).limit(8).all() + toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).order_by(db.desc(Tag.id)).all() + popular = {} + new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all() + for toplevel_tag in toplevel_tags: + popular[toplevel_tag.name] = join(query.filter(Package.tags.any(name=toplevel_tag.name)).order_by(db.desc(Package.score))).limit(8).all() high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \ .filter(Package.reviews.any()).limit(4).all() @@ -50,5 +49,5 @@ def game_hub(package: Package): updated = updated[:4] return render_template("packages/game_hub.html", package=package, count=count, - new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam, + new=new, updated=updated, popular=popular, toplevel=toplevel_tags, high_reviewed=high_reviewed) diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index 89d1efe..016522d 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -99,10 +99,12 @@ def list_all(): selected_tags = set(qb.tags) + toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all() + return render_template("packages/list.html", query_hint=title, packages=query.items, pagination=query, query=search, tags=tags, selected_tags=selected_tags, type=type_name, - authors=authors, packages_count=query.total, topics=topics) + authors=authors, packages_count=query.total, topics=topics, toplevel=toplevel_tags) def getReleases(package): @@ -120,7 +122,7 @@ def view(package): package.checkPerm(current_user, Permission.APPROVE_NEW)) conflicting_modnames = None - if show_similar and package.type != PackageType.ASSETPACK: + if show_similar: conflicting_modnames = db.session.query(MetaPackage.name) \ .filter(MetaPackage.id.in_([ mp.id for mp in package.provides ])) \ .filter(MetaPackage.packages.any(Package.id != package.id)) \ @@ -136,14 +138,12 @@ def view(package): conflicting_modnames = set([x[0] for x in conflicting_modnames]) packages_uses = None - if package.type == PackageType.TOOL: - packages_uses = Package.query.filter( - Package.type == PackageType.TOOL, - Package.id != package.id, - Package.state == PackageState.APPROVED, - Package.dependencies.any( - Dependency.meta_package_id.in_([p.id for p in package.provides]))) \ - .order_by(db.desc(Package.score)).limit(6).all() + packages_uses = Package.query.filter( + Package.id != package.id, + Package.state == PackageState.APPROVED, + Package.dependencies.any( + Dependency.meta_package_id.in_([p.id for p in package.provides]))) \ + .order_by(db.desc(Package.score)).limit(6).all() releases = getReleases(package) @@ -178,11 +178,13 @@ def view(package): has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0 + toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all() + return render_template("packages/view.html", package=package, releases=releases, packages_uses=packages_uses, conflicting_modnames=conflicting_modnames, review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, - threads=threads.all(), has_review=has_review) + threads=threads.all(), has_review=has_review, toplevel=toplevel_tags) @bp.route("/packages///shields//") @@ -226,7 +228,14 @@ def makeLabel(obj): class PackageForm(FlaskForm): - type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.GAME) + try: + toplevel_tags = [ x.title for x in Tag.query.filter_by(is_toplevel=True).all() ] + type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=toplevel_tags, default=toplevel_tags[0]) + except: + toplevel_tags = [] + # type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=toplevel_tags, default=toplevel_tags[0]) + + # type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.GAME) title = StringField(lazy_gettext("Title (Human-readable)"), [InputRequired(), Length(1, 100)]) name = StringField(lazy_gettext("Name (Technical)"), [InputRequired(), Length(1, 100), Regexp("^[a-zA-Z0-9_\-\.]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_), dashes and periods only"))]) short_desc = StringField(lazy_gettext("Short Description (Plaintext)"), [InputRequired(), Length(1,200)]) @@ -293,9 +302,6 @@ def create_edit(author=None, name=None): form.tags.data = package.tags form.content_warnings.data = package.content_warnings - if request.method == "POST" and form.type.data == PackageType.ASSETPACK: - form.license.data = form.media_license.data - if form.validate_on_submit(): wasNew = False if not package: @@ -353,7 +359,7 @@ def create_edit(author=None, name=None): form=form, author=author, enable_wizard=enableWizard, packages=package_query.all(), mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all(), - tabs=get_package_tabs(current_user, package), current_tab="edit") + tabs=get_package_tabs(current_user, package), current_tab="edit", toplevel=get_toplevel_tags()) @bp.route("/packages///state/", methods=["POST"]) @@ -408,7 +414,7 @@ def move_to_state(package): def remove(package): if request.method == "GET": return render_template("packages/remove.html", package=package, - tabs=get_package_tabs(current_user, package), current_tab="remove") + tabs=get_package_tabs(current_user, package), current_tab="remove", toplevel=get_toplevel_tags()) reason = request.form.get("reason") or "?" @@ -501,7 +507,7 @@ def edit_maintainers(package): users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).order_by(db.asc(User.username)).all() return render_template("packages/edit_maintainers.html", package=package, form=form, - users=users, tabs=get_package_tabs(current_user, package), current_tab="maintainers") + users=users, tabs=get_package_tabs(current_user, package), current_tab="maintainers", toplevel=get_toplevel_tags()) @bp.route("/packages///remove-self-maintainer/", methods=["POST"]) @@ -540,7 +546,7 @@ def audit(package): pagination = query.paginate(page, num, True) return render_template("packages/audit.html", log=pagination.items, pagination=pagination, - package=package, tabs=get_package_tabs(current_user, package), current_tab="audit") + package=package, tabs=get_package_tabs(current_user, package), current_tab="audit", toplevel=get_toplevel_tags()) class PackageAliasForm(FlaskForm): @@ -554,7 +560,7 @@ class PackageAliasForm(FlaskForm): @rank_required(UserRank.EDITOR) @is_package_page def alias_list(package: Package): - return render_template("packages/alias_list.html", package=package) + return render_template("packages/alias_list.html", package=package, toplevel=get_toplevel_tags()) @bp.route("/packages///aliases/new/", methods=["GET", "POST"]) @@ -580,7 +586,7 @@ def alias_create_edit(package: Package, alias_id: int = None): return redirect(package.getURL("packages.alias_list")) - return render_template("packages/alias_create_edit.html", package=package, form=form) + return render_template("packages/alias_create_edit.html", package=package, form=form, toplevel=get_toplevel_tags()) @bp.route("/packages///share/") @@ -588,7 +594,7 @@ def alias_create_edit(package: Package, alias_id: int = None): @is_package_page def share(package): return render_template("packages/share.html", package=package, - tabs=get_package_tabs(current_user, package), current_tab="share") + tabs=get_package_tabs(current_user, package), current_tab="share", toplevel=get_toplevel_tags()) @bp.route("/packages///similar/") @@ -610,4 +616,4 @@ def similar(package): # .all() return render_template("packages/similar.html", package=package, - packages_modnames=packages_modnames, similar_topics=[]) + packages_modnames=packages_modnames, similar_topics=[], toplevel=get_toplevel_tags()) diff --git a/app/blueprints/packages/releases.py b/app/blueprints/packages/releases.py index 7732a86..2503e54 100644 --- a/app/blueprints/packages/releases.py +++ b/app/blueprints/packages/releases.py @@ -35,7 +35,7 @@ from . import bp, get_package_tabs def list_releases(package): return render_template("packages/releases_list.html", package=package, - tabs=get_package_tabs(current_user, package), current_tab="releases") + tabs=get_package_tabs(current_user, package), current_tab="releases", toplevel=get_toplevel_tags()) # def get_mt_releases(is_max): @@ -92,7 +92,7 @@ def create_release(package): except LogicError as e: flash(e.message, "danger") - return render_template("packages/release_new.html", package=package, form=form) + return render_template("packages/release_new.html", package=package, form=form, toplevel=get_toplevel_tags()) @bp.route("/packages///releases//download/") @@ -163,7 +163,7 @@ def edit_release(package, id): db.session.commit() return redirect(package.getURL("packages.list_releases")) - return render_template("packages/release_edit.html", package=package, release=release, form=form) + return render_template("packages/release_edit.html", package=package, release=release, form=form, toplevel=get_toplevel_tags()) @@ -203,7 +203,7 @@ def bulk_change_release(package): return redirect(package.getURL("packages.list_releases")) - return render_template("packages/release_bulk_change.html", package=package, form=form) + return render_template("packages/release_bulk_change.html", package=package, form=form, toplevel=get_toplevel_tags()) @bp.route("/packages///releases//delete/", methods=["POST"]) @@ -301,7 +301,7 @@ def update_config(package): return redirect(package.getURL("packages.list_releases")) - return render_template("packages/update_config.html", package=package, form=form) + return render_template("packages/update_config.html", package=package, form=form, toplevel=get_toplevel_tags()) @bp.route("/packages///setup-releases/") @@ -314,7 +314,7 @@ def setup_releases(package): if package.update_config: return redirect(package.getURL("packages.update_config")) - return render_template("packages/release_wizard.html", package=package) + return render_template("packages/release_wizard.html", package=package, toplevel=get_toplevel_tags()) @bp.route("/user/update-configs/") @@ -343,4 +343,4 @@ def bulk_update_config(username=None): Package.update_config.has()) \ .order_by(db.asc(Package.title)).all() - return render_template("packages/bulk_update_conf.html", user=user, confs=confs, form=form) + return render_template("packages/bulk_update_conf.html", user=user, confs=confs, form=form, toplevel=get_toplevel_tags()) diff --git a/app/blueprints/packages/reviews.py b/app/blueprints/packages/reviews.py index a5b194c..5968b52 100644 --- a/app/blueprints/packages/reviews.py +++ b/app/blueprints/packages/reviews.py @@ -26,7 +26,7 @@ from wtforms import * from wtforms.validators import * from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \ Permission, AuditSeverity -from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog +from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog, get_toplevel_tags from app.tasks.webhooktasks import post_discord_webhook @@ -36,7 +36,7 @@ def list_reviews(): num = min(40, get_int_or_abort(request.args.get("n"), 100)) pagination = PackageReview.query.order_by(db.desc(PackageReview.created_at)).paginate(page, num, True) - return render_template("packages/reviews_list.html", pagination=pagination, reviews=pagination.items) + return render_template("packages/reviews_list.html", pagination=pagination, reviews=pagination.items, toplevel=get_toplevel_tags()) class ReviewForm(FlaskForm): @@ -123,7 +123,7 @@ def review(package): return redirect(package.getURL("packages.view")) return render_template("packages/review_create_edit.html", - form=form, package=package, review=review) + form=form, package=package, review=review, toplevel=get_toplevel_tags()) @bp.route("/packages///reviews//delete/", methods=["POST"]) @@ -237,4 +237,4 @@ def review_votes(package): user_biases_info.sort(key=lambda x: -abs(x.balance)) return render_template("packages/review_votes.html", form=form, package=package, reviews=package.reviews, - user_biases=user_biases_info) + user_biases=user_biases_info, toplevel=get_toplevel_tags()) diff --git a/app/blueprints/packages/screenshots.py b/app/blueprints/packages/screenshots.py index b6a2cc8..eb08ca3 100644 --- a/app/blueprints/packages/screenshots.py +++ b/app/blueprints/packages/screenshots.py @@ -73,7 +73,7 @@ def screenshots(package): db.session.commit() return render_template("packages/screenshots.html", package=package, form=form, - tabs=get_package_tabs(current_user, package), current_tab="screenshots") + tabs=get_package_tabs(current_user, package), current_tab="screenshots", toplevel=get_toplevel_tags()) @bp.route("/packages///screenshots/new/", methods=["GET", "POST"]) @@ -92,7 +92,7 @@ def create_screenshot(package): except LogicError as e: flash(e.message, "danger") - return render_template("packages/screenshot_new.html", package=package, form=form) + return render_template("packages/screenshot_new.html", package=package, form=form, toplevel=get_toplevel_tags()) @bp.route("/packages///screenshots//edit/", methods=["GET", "POST"]) @@ -124,7 +124,7 @@ def edit_screenshot(package, id): db.session.commit() return redirect(package.getURL("packages.screenshots")) - return render_template("packages/screenshot_edit.html", package=package, screenshot=screenshot, form=form) + return render_template("packages/screenshot_edit.html", package=package, screenshot=screenshot, form=form, toplevel=get_toplevel_tags()) @bp.route("/packages///screenshots//delete/", methods=["POST"]) diff --git a/app/blueprints/users/account.py b/app/blueprints/users/account.py index 274340c..10a7bc4 100644 --- a/app/blueprints/users/account.py +++ b/app/blueprints/users/account.py @@ -15,7 +15,9 @@ # along with this program. If not, see . - +import base64 +import string +import random from flask import * from flask_babel import gettext, lazy_gettext, get_locale from flask_login import current_user, login_required, logout_user, login_user @@ -23,6 +25,7 @@ from flask_wtf import FlaskForm from sqlalchemy import or_ from wtforms import * from wtforms.validators import * +from captcha.image import ImageCaptcha from app.models import * from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email @@ -105,13 +108,13 @@ class RegisterForm(FlaskForm): Regexp("^[a-zA-Z0-9._-]+$", message=lazy_gettext("Only a-zA-Z0-9._ allowed"))]) email = StringField(lazy_gettext("Email"), [InputRequired(), Email()]) password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(6, 100)]) - question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()]) + question = StringField(lazy_gettext("Type the characters from the image above."), [InputRequired()]) agree = BooleanField(lazy_gettext("I agree"), [DataRequired()]) submit = SubmitField(lazy_gettext("Register")) def handle_register(form): - if form.question.data.strip().lower() != "19": + if form.question.data.strip().lower() != session["captcha-solution"]: flash(gettext("Incorrect captcha answer"), "danger") return @@ -181,7 +184,13 @@ def register(): if ret: return ret - return render_template("users/register.html", form=form, + image = ImageCaptcha() + characters = string.ascii_letters + string.digits + solution = ''.join(random.choice(characters) for i in range(4)) + data = image.generate(solution) + session["captcha-solution"] = solution + b64data = "data:image/png;base64," + base64.b64encode(data.read()).decode("utf-8") + return render_template("users/register.html", form=form, captcha=b64data, suggested_password=genphrase(entropy=52, wordset="bip39")) diff --git a/app/blueprints/users/profile.py b/app/blueprints/users/profile.py index c81bcd2..045c4ba 100644 --- a/app/blueprints/users/profile.py +++ b/app/blueprints/users/profile.py @@ -151,24 +151,17 @@ def get_user_medals(user: User) -> Tuple[List[Medal], List[Medal]]: .filter(text("rank <= 30")) \ .all() - user_package_ranks = next( - (x for x in user_package_ranks if x[0] == PackageType.TOOL or x[2] <= 10), - None) + user_package_ranks = next((x for x in user_package_ranks if x[2] <= 10), None) if user_package_ranks: top_rank = user_package_ranks[2] - top_type = PackageType.coerce(user_package_ranks[0]) + # top_type = PackageType.coerce(user_package_ranks[0]) if top_rank == 1: - title = gettext(u"Top %(type)s", type=top_type.text.lower()) + title = gettext(u"Top projects", type=top_type.text.lower()) else: - title = gettext(u"Top %(group)d %(type)s", group=top_rank, type=top_type.text.lower()) - if top_type == PackageType.TOOL: - icon = "fa-box" - elif top_type == PackageType.GAME: - icon = "fa-gamepad" - else: - icon = "fa-paint-brush" + title = gettext(u"Top %(group)d projects", group=top_rank, type=top_type.text.lower()) + icon = "fa-paint-brush" - description = gettext(u"%(display_name)s has a %(type)s placed at #%(place)d.", + description = gettext(u"%(display_name)s has a projects placed at #%(place)d.", display_name=user.display_name, type=top_type.text.lower(), place=top_rank) unlocked.append( Medal.make_unlocked(place_to_color(top_rank), icon, title, description)) diff --git a/app/default_data.py b/app/default_data.py index 9d5cd80..5025ff0 100644 --- a/app/default_data.py +++ b/app/default_data.py @@ -36,11 +36,11 @@ def populate(session): featured.is_protected = True # These tags replace "package types" - game_tag = Tag("Game") + game_tag = Tag("Games") game_tag.is_toplevel = True - tool_tag = Tag("Tool") + tool_tag = Tag("Tools") tool_tag.is_toplevel = True - mod_tag = Tag("Mod") + mod_tag = Tag("Mods") mod_tag.is_toplevel = True session.add(featured) session.add(game_tag) @@ -110,7 +110,7 @@ def populate_test_data(session): tool.title = "Alpha Test" tool.license = licenses["MIT"] tool.media_license = licenses["MIT"] - tool.type = PackageType.TOOL + # tool.type = PackageType.TOOL tool.author = admin_user tool.tags.append(tags["mapgen"]) tool.tags.append(tags["environment"]) @@ -134,7 +134,7 @@ def populate_test_data(session): mod1.title = "Awards" mod1.license = licenses["LGPLv2.1"] mod1.media_license = licenses["MIT"] - mod1.type = PackageType.TOOL + # mod1.type = PackageType.TOOL mod1.author = admin_user mod1.tags.append(tags["player_effects"]) mod1.repo = "https://github.com/libregaming/awards" @@ -170,7 +170,7 @@ awards.register_achievement("award_mesefind",{ mod2.name = "mesecons" mod2.title = "Mesecons" mod2.tags.append(tags["tools"]) - mod2.type = PackageType.TOOL + # mod2.type = PackageType.TOOL mod2.license = licenses["LGPLv3"] mod2.media_license = licenses["MIT"] mod2.author = jeija @@ -260,7 +260,7 @@ No warranty is provided, express or implied, for any part of the project. tool.title = "Handholds" tool.license = licenses["MIT"] tool.media_license = licenses["MIT"] - tool.type = PackageType.TOOL + # tool.type = PackageType.TOOL tool.author = ez tool.tags.append(tags["player_effects"]) tool.repo = "https://github.com/ezhh/handholds" @@ -284,7 +284,7 @@ No warranty is provided, express or implied, for any part of the project. tool.title = "Other Worlds" tool.license = licenses["MIT"] tool.media_license = licenses["MIT"] - tool.type = PackageType.TOOL + # tool.type = PackageType.TOOL tool.author = ez tool.tags.append(tags["mapgen"]) tool.tags.append(tags["environment"]) @@ -301,7 +301,7 @@ No warranty is provided, express or implied, for any part of the project. tool.title = "Food" tool.license = licenses["LGPLv2.1"] tool.media_license = licenses["MIT"] - tool.type = PackageType.TOOL + # tool.type = PackageType.TOOL tool.author = admin_user tool.tags.append(tags["player_effects"]) tool.repo = "https://github.com/libregaming/food/" @@ -317,7 +317,7 @@ No warranty is provided, express or implied, for any part of the project. tool.title = "Sweet Foods" tool.license = licenses["CC0"] tool.media_license = licenses["MIT"] - tool.type = PackageType.TOOL + # tool.type = PackageType.TOOL tool.author = admin_user tool.tags.append(tags["player_effects"]) tool.repo = "https://github.com/libregaming/food_sweet/" @@ -332,7 +332,7 @@ No warranty is provided, express or implied, for any part of the project. game1.state = PackageState.APPROVED game1.name = "capturetheflag" game1.title = "Capture The Flag" - game1.type = PackageType.GAME + # game1.type = PackageType.GAME game1.license = licenses["LGPLv2.1"] game1.media_license = licenses["MIT"] game1.author = admin_user @@ -397,7 +397,7 @@ Uses the CTF PvP Engine. tool.title = "PixelBOX Reloaded" tool.license = licenses["CC0"] tool.media_license = licenses["MIT"] - tool.type = PackageType.ASSETPACK + # tool.type = PackageType.ASSETPACK tool.author = admin_user tool.forums = 14132 tool.short_desc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit" @@ -413,16 +413,16 @@ Uses the CTF PvP Engine. session.commit() - metas = {} - for package in Package.query.filter_by(type=PackageType.TOOL).all(): - meta = None - try: - meta = metas[package.name] - except KeyError: - meta = MetaPackage(package.name) - session.add(meta) - metas[package.name] = meta - package.provides.append(meta) + # metas = {} + # for package in Package.query.filter_by(type=PackageType.TOOL).all(): + # meta = None + # try: + # meta = metas[package.name] + # except KeyError: + # meta = MetaPackage(package.name) + # session.add(meta) + # metas[package.name] = meta + # package.provides.append(meta) - dep = Dependency(food_sweet, meta=metas["food"]) - session.add(dep) + # dep = Dependency(food_sweet, meta=metas["food"]) + # session.add(dep) diff --git a/app/flatpages/help/api.md b/app/flatpages/help/api.md index cab3943..08b69e1 100644 --- a/app/flatpages/help/api.md +++ b/app/flatpages/help/api.md @@ -390,9 +390,8 @@ Supported query parameters: * `downloads`: get number of downloads * `new`: new packages * `updated`: recently updated packages - * `pop_mod`: popular mods - * `pop_txp`: popular textures - * `pop_game`: popular games + * `popular`: popular packages + * `toplevel`: toplevel nav tags * `high_reviewed`: highest reviewed * GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning) * `featured`: featured games diff --git a/app/flatpages/help/faq.md b/app/flatpages/help/faq.md index 6713647..5754a8d 100644 --- a/app/flatpages/help/faq.md +++ b/app/flatpages/help/faq.md @@ -13,7 +13,7 @@ If you don't, then you can just sign up using an email address and password. GitHub can only be used to login, not to register. -Register +Register ### My verification email never arrived diff --git a/app/logic/game_support.py b/app/logic/game_support.py index 76824d7..7e71ace 100644 --- a/app/logic/game_support.py +++ b/app/logic/game_support.py @@ -20,7 +20,7 @@ import sys from typing import List, Dict, Optional, Iterator, Iterable from app.logic.LogicError import LogicError -from app.models import Package, MetaPackage, PackageType, PackageState, PackageGameSupport, db +from app.models import Package, MetaPackage, PackageState, PackageGameSupport, db """ get_game_support(package): @@ -128,39 +128,36 @@ class GameSupportResolver: history = history.copy() history.append(key) - if package.type == PackageType.GAME: - return PackageSet([package]) + # if package.type == PackageType.GAME: + return PackageSet([package]) - if key in self.resolved_packages: - return self.resolved_packages.get(key) + # if key in self.resolved_packages: + # return self.resolved_packages.get(key) - if key in self.checked_packages: - print(f"Error, cycle found: {','.join(history)}", file=sys.stderr) - return PackageSet() + # if key in self.checked_packages: + # print(f"Error, cycle found: {','.join(history)}", file=sys.stderr) + # return PackageSet() - self.checked_packages.add(key) + # self.checked_packages.add(key) - if package.type != PackageType.TOOL: - raise LogicError(500, "Got non-tool") + # retval = PackageSet() - retval = PackageSet() + # for dep in package.dependencies.filter_by(optional=False).all(): + # ret = self.resolve_for_meta_package(dep.meta_package, history) + # if len(ret) == 0: + # continue + # elif len(retval) == 0: + # retval.update(ret) + # else: + # retval.intersection_update(ret) + # if len(retval) == 0: + # raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games") - for dep in package.dependencies.filter_by(optional=False).all(): - ret = self.resolve_for_meta_package(dep.meta_package, history) - if len(ret) == 0: - continue - elif len(retval) == 0: - retval.update(ret) - else: - retval.intersection_update(ret) - if len(retval) == 0: - raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games") - - self.resolved_packages[key] = retval - return retval + # self.resolved_packages[key] = retval + # return retval def update_all(self) -> None: - for package in Package.query.filter(Package.type == PackageType.TOOL, Package.state != PackageState.DELETED).all(): + for package in Package.query.filter(Package.state != PackageState.DELETED).all(): retval = self.resolve(package, []) for game in retval: support = PackageGameSupport(package, game) diff --git a/app/logic/packages.py b/app/logic/packages.py index d8343df..8c88683 100644 --- a/app/logic/packages.py +++ b/app/logic/packages.py @@ -20,7 +20,7 @@ import validators from flask_babel import lazy_gettext from app.logic.LogicError import LogicError -from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \ +from app.models import User, Package, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \ License, UserRank, PackageDevState from app.utils import addAuditLog from app.utils.url import clean_youtube_url @@ -118,8 +118,8 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool, validate(data) - if "type" in data: - data["type"] = PackageType.coerce(data["type"]) + # if "type" in data: + # data["type"] = PackageType.coerce(data["type"]) if "dev_state" in data: data["dev_state"] = PackageDevState.coerce(data["dev_state"]) @@ -140,12 +140,12 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool, if key in data: setattr(package, key, data[key]) - if package.type == PackageType.ASSETPACK: - package.license = package.media_license + # if package.type == PackageType.ASSETPACK: + # package.license = package.media_license - if was_new and package.type == PackageType.TOOL: - m = MetaPackage.GetOrCreate(package.name, {}) - package.provides.append(m) + # if was_new and package.type == PackageType.TOOL: + # m = MetaPackage.GetOrCreate(package.name, {}) + # package.provides.append(m) if "tags" in data: old_tags = list(package.tags) diff --git a/app/models/__init__.py b/app/models/__init__.py index 8b9cfab..ea8e2a2 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -120,7 +120,7 @@ class ForumTopic(db.Model): wip = db.Column(db.Boolean, default=False, nullable=False) discarded = db.Column(db.Boolean, default=False, nullable=False) - type = db.Column(db.Enum(PackageType), nullable=False) + # type = db.Column(db.Enum(PackageType), nullable=False) title = db.Column(db.String(200), nullable=False) name = db.Column(db.String(30), nullable=True) link = db.Column(db.String(200), nullable=True) diff --git a/app/models/packages.py b/app/models/packages.py index 85a8308..c2ffe59 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -48,49 +48,49 @@ class License(db.Model): return self.name -class PackageType(enum.Enum): - GAME = "Game" - TOOL = "Tool" - ASSETPACK = "Asset Pack" +# class PackageType(enum.Enum): +# GAME = "Game" +# TOOL = "Tool" +# ASSETPACK = "Asset Pack" - def toName(self): - return self.name.lower() +# def toName(self): +# return self.name.lower() - def __str__(self): - return self.name +# def __str__(self): +# return self.name - @property - def text(self): - if self == PackageType.TOOL: - return lazy_gettext("Tool") - elif self == PackageType.GAME: - return lazy_gettext("Game") - elif self == PackageType.ASSETPACK: - return lazy_gettext("Asset Pack") +# @property +# def text(self): +# if self == PackageType.TOOL: +# return lazy_gettext("Tool") +# elif self == PackageType.GAME: +# return lazy_gettext("Game") +# elif self == PackageType.ASSETPACK: +# return lazy_gettext("Asset Pack") - @property - def plural(self): - if self == PackageType.TOOL: - return lazy_gettext("Tools") - elif self == PackageType.GAME: - return lazy_gettext("Games") - elif self == PackageType.ASSETPACK: - return lazy_gettext("Asset Packs") +# @property +# def plural(self): +# if self == PackageType.TOOL: +# return lazy_gettext("Tools") +# elif self == PackageType.GAME: +# return lazy_gettext("Games") +# elif self == PackageType.ASSETPACK: +# return lazy_gettext("Asset Packs") - @classmethod - def get(cls, name): - try: - return PackageType[name.upper()] - except KeyError: - return None +# @classmethod +# def get(cls, name): +# try: +# return PackageType[name.upper()] +# except KeyError: +# return None - @classmethod - def choices(cls): - return [(choice, choice.text) for choice in cls] +# @classmethod +# def choices(cls): +# return [(choice, choice.text) for choice in cls] - @classmethod - def coerce(cls, item): - return item if type(item) == PackageType else PackageType[item.upper()] +# @classmethod +# def coerce(cls, item): +# return item if type(item) == PackageType else PackageType[item.upper()] class PackageDevState(enum.Enum): @@ -218,7 +218,7 @@ class PackagePropertyKey(enum.Enum): title = "Title" short_desc = "Short Description" desc = "Description" - type = "Type" + # type = "Type" license = "License" media_license = "Media License" tags = "Tags" @@ -378,7 +378,7 @@ class Package(db.Model): desc = db.Column(db.UnicodeText, nullable=True) build_desc = db.Column(db.UnicodeText, nullable=True) install_desc = db.Column(db.UnicodeText, nullable=True) - type = db.Column(db.Enum(PackageType), nullable=False) + # type = db.Column(db.Enum(PackageType), nullable=False) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) approved_at = db.Column(db.DateTime, nullable=True, default=None) @@ -534,7 +534,7 @@ class Package(db.Model): "title": self.title, "author": self.author.username, "short_description": short_desc, - "type": self.type.toName(), + # "type": self.type.toName(), "release": release_id, "thumbnail": (base_url + tnurl) if tnurl is not None else None, "aliases": [ alias.getAsDictionary() for alias in self.aliases ], @@ -559,7 +559,7 @@ class Package(db.Model): "title": self.title, "short_description": self.short_desc, "long_description": self.desc, - "type": self.type.toName(), + # "type": self.type.toName(), "created_at": self.created_at.isoformat(), "license": self.license.name, @@ -771,7 +771,7 @@ class MetaPackage(db.Model): dependencies = db.relationship("Dependency", back_populates="meta_package", lazy="dynamic") packages = db.relationship("Package", lazy="dynamic", back_populates="provides", secondary=PackageProvides) - mp_name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'") + mp_name_valid = db.CheckConstraint("name ~* '^[a-zA-Z0-9_\-\.]+$'") def __init__(self, name=None): self.name = name @@ -872,48 +872,6 @@ class Tag(db.Model): } -# class MinetestRelease(db.Model): -# id = db.Column(db.Integer, primary_key=True) -# name = db.Column(db.String(100), unique=True, nullable=False) -# protocol = db.Column(db.Integer, nullable=False, default=0) - -# def __init__(self, name=None, protocol=0): -# self.name = name -# self.protocol = protocol - -# def getActual(self): -# return None if self.name == "None" else self - -# def getAsDictionary(self): -# return { -# "name": self.name, -# "protocol_version": self.protocol, -# "is_dev": "-dev" in self.name, -# } - -# @classmethod -# def get(cls, version, protocol_num): -# if version: -# parts = version.strip().split(".") -# if len(parts) >= 2: -# major_minor = parts[0] + "." + parts[1] -# query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor))) -# if protocol_num: -# query = query.filter_by(protocol=protocol_num) - -# release = query.one_or_none() -# if release: -# return release - -# if protocol_num: -# # Find the closest matching release -# return MinetestRelease.query.order_by(db.desc(MinetestRelease.protocol), -# db.desc(MinetestRelease.id)) \ -# .filter(MinetestRelease.protocol <= protocol_num).first() - -# return None - - class PackageRelease(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -929,11 +887,6 @@ class PackageRelease(db.Model): downloads = db.Column(db.Integer, nullable=False, default=0) channel = db.Column(db.String(200), nullable=False, default="") - # min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None) - # min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id]) - - # max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None) - # max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id]) # If the release is approved, then the task_id must be null and the url must be present CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)") @@ -951,8 +904,6 @@ class PackageRelease(db.Model): "commit": self.commit_hash, "downloads": self.downloads, "channel": self.channel, - # "min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(), - # "max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(), } def getLongAsDictionary(self): @@ -963,8 +914,6 @@ class PackageRelease(db.Model): "release_date": self.releaseDate.isoformat(), "commit": self.commit_hash, "downloads": self.downloads, - # "min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(), - # "max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(), "channel": self.channel, "package": self.package.getAsDictionaryKey() } diff --git a/app/public/static/lg-logo.png b/app/public/static/lg-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b46d8a578a2f7d3e08b15c083a72cd746070a1a9 GIT binary patch literal 16601 zcmc(H<6~Xj6Ye=Nn-kl%ZKqL#Hnwf^#I_o;EAf>m9OO4CSJ`(&Ia|SZUlDr`{AAAXx zXA8=%C8XAyMHsX}D~DdLNCm(*fT1jGZA-N)?Ofd%9|XF;ZP*3G&#N&1@U{PudqFKL z=DyTv|6}82{if?s7m46Y7`agvh?gLC*imfXfro4eKB0dnoPb*5^MTX<^9OOO0O=jg z7PFY#s-Fos?%SnXR{Vn9O?`4^ZAeOxd4zoP%3ddE>xwd73NmjslMV-Us$eE%j+tm5bT51xgf=b`M<|M~+5fbbKWK2N-7E&e{p zEG$J2{V8L@v?QLEUlFSJ*~BM}Otp4Qo}uw033KcM%#7?$dZK2;Zp5 zCP3*Zkl#rMwM$r9-a!n5I(3X8quEg2)YhsjITQ8@4bAzhVkn1{$eoBpv64#^rZf19 z@-q1~rZTh2X4Hz#;lJ|SkA-}rHup9}cmiBuv?eDC6@oZ|;((t>Rt&>40BO+*7#jo; zXqhDJAK{uHT!1uyv%d^C6qbaObef5xQ}?nh!?EUfV?4xyu|3-FiZDe4!Vy5`eQ_(A zX#S)!6Ax)5T7(?2hI)jF72WnkVK1!30^r(4vOw~7 z=_3X+j|zSPzEDBf1&nlsEat&4ul>QqX<}_#*4gim2(ji`oHuh}Vyp`6B}>E>{|N|^ zzzK;Mc_5n=YeHw40JuT;AR_41Z)0VSom~rl#7{hjZJpZ)We8FyXfI8JNIo(GaxN5$ z;?sqI4fVhaStWG{0oVZ^z{t7o<~y8XH{)=WON5W^e^%a}G^%gVo*kapdU# z)yI6-;t>-Y@)Zi^nHp-iMi=MJkOxF17syUFp-xd67P#PYRC63&>y0qbJ-u* zv9?fSs6}_|yNtsW>Owml5j`_$`Va}L3G@bbAcS!5hH|vhCY&vIH^W!{>*5jCo_0P^ z7fMseMJgNvFhMv{4$zWVMbh*6HnCjHasrY-qWVR=w`!h6@sI@+6v zXYEN}>DhXXUsoqn#X6%}(1V!Uq7yvcMh~T^q~<57FH!q^;HGa|T6FR+L7HC~K~3-; zy-M9*VOQrhcg{Sow+9s&wAC&>LR0%mI1M@IRFDDo5EGn$Vu;enu*a-OhsJqmBlDIV z(#bFG6o_{A9yxAsQ?AnKTEu^CC!`U)V;@oHm^%kFnVjGLGJjLZi2NtxO%EQM&K?Nz zfCJTo?-4X$uW8Y~nXyN3t1+j~z+l*W>*DKmQkC1jId5=&Kc zHezkojQ#fNP8PC$>zOA6OwaUEL+?V*6(Z3TsTO}`vN@a@<@DniXM%Fv$)Hw=(6JqL zE2b->PyBJ4(xAyE^V5b5jxWfin0>Bdyb!b6^=^SNePQ`$mXOiGQ_kzRc#Yz70b*e+ z2#SV)WtyD?u+Eu!y267lXT2{C8hWFT5$1_%8pIm0+aZ+A8e!!+`$- z_S1w!=CLp{>bgm=s#i0vefi->C#iU0=j9{kB%&pMdNL5B^-FX)U_&wn-is_-8R|Du zwE|Zb3Cq)T-0E_>d|EZ@wv*TegdfFc8OT|XxOtt@|f91oaZ4x5RrBfGp(b~pHY&mUkU)^+e)GtJJNOeFbkF+n*% z4$A@LDU>#r#`n%enD{9<5Zww*h+t32?VFoA-`5AjW}^=MY!dYk{S#-+fs+);I2rCs z?VW<2tlEqY-6nn`<1-T|>m1$2!!~g4J;m9u#_wS>kZt8#A zFJ%8DNC0163XP1Q#CCmYnYv_J&tE@vo zm}ZLK2mqdfZySdFPsHvMq^mQz_iKY3s83&yiG#zKA$8zJz&4^kT`PS7?qU`Yu5cpk zCfXtmeFc!fw(T{Bee(qLgk;f4FB!Iw3VnF=c|b*w5^Sc7@rhGDI`uS zzrgN`k6mlqM#_MpfI5U>rnfEB$?Sdg2HVwho+m#xU;fLxU8Q$^QuCyuRCqti*(<#9z~ zCh#MyKW_xSY#mhR_CQz$P| zbu9#giCx%QxDl``=HZ;v>XHUUBccq(VS#>t0cg-k&pX!0Ag-pyMwrX9^Xpi#xUx38 zlI+D;e2d9KD0J7w2XeTa=HQv#0^#%q%#rj;*4xU-;f0-(F2>8ZUwF z+r|yzJ5Ha*9QRPHbiuV%&j$_oA26E9Q#5grEVC=b{d9$}krpz)Na(eL|Dg9BFaS~Z zswO6Ar;!Go8~B*5vn|}{HR5?(dPb*yyObnFp7Lytqo@BYX2M?c%I^1f;sgJ)IVl$- z&E&uxifzu($U91WnhG61>WSsdb4JQrcPq(5RQ`A{Q4>X-uyWw~%)F?P;0}lM#|E4Z_!NdMfRWy;Bvp$1k~0OWn?7-+lTR9H`O& z1p3$I`SF*6Es8iuzmII_EY~}JjxM^qJ<>K$UI!^`>_2X8H z4Pncf0KDv0>9I}Qf}|M|h&Fb_m!68ik6Xz1 z2AVQAM?Vo~4d8M2`Jx)v=jnp@qJLTq*H+NT7Fi1@$+z5v424yt;G?nw&kZMiuHTy! zgx$z6khS25Inq+i%>l7cZ)v)>=+8&nh-AQA_I{*^|tSd%e2}X-k{du-;JCa-{$;p8DO)^)%RGCxf$# znw{MyL(lBC;<7mI>-RM0JdSsbwavMfZk=N>Q1bVs&}De#bD08_?<=jQLnOR|8vJZl z{Gpsul>q)iu0jit?Qm%kRo|duxQ}W=U|&Su--sWS24mb|zc7ZODh=yDoc>s$IIy5M z;=Yl(iG9~qJ?^jJvf_`F>ATUyZba?Jv%esfc`4v3t=MvvKL*ni05%e3_4{VKLyFd3O(F?>&zxJ19(a*O_+_=q4A$hBDG?PvT z8Or~%*%lRvV+p+43!-!ro8QbiCacw^d^Eg=&ks>?o;O2s@5|&|dx9(C9<})4f{{MC zSQ>)^_t~Msn3*}iMx5twLx+-vT_|CMO&tYuYF**K9yh>4Fq48O3-s^N%tisfhr9If zlHOE&k`&zodQtlFiH8k=xj*4XA}xPx&%Tj%;WjctlRtm7WEJF^V};L7&Ff><;5iQOVrQlMs?o z=Xd?5^vt0HQJoABlnZ!O&>V;yrVzNXe69XXxd3KP6>B(;x~Wf8OVQ05-hCpp?{1*C zVkfu=X};JDvbl5>@g5b62J^#Nt0A|G(21I|%0!wlfxmf@?3U1-{*THOAASOJ#7E+kgk2qId2Q$&8R*rqsOX5yBD zI~FH5=-_r>id#A=D;`mrm1nd$Hj%VV2u z-jDSL#Ps#EnrGI$LQ4Fe;`9KC-Puof{;%0pQhAB^y>YOtk)@I9DoK>8WOX3;xE)>(?x$w zBl_5w%_w8@K?ULarEUs1igb2DvcpkXJ|03lo0BCcHbpkh^t;F=Nec)sGK-9R4x~V_N{)(rk z_*CRRp8B$) z+`G9lyYeiY+x2vkXsz^@hrJD*)9Kf3Z5ac0#bM*D98DyAj6VCRD!;M zJuqvg$hV#0Vr_N^!xWz`XCI37AfCFRcx?fxcG4h*19dkW+#_#BuG-avjD;3f_+w*| z|AL_Z5xhKc^5sMW?wvoeE?ap~=o&AK)UwW5`3nlbpz@69#3Iws6He_V;dC7_Q=o6nJ-7Am1UvuIUSz#L!cYF^PP#;?{@`*hY6)LM zS;AtxOL5ABMCe34=*tiPJ-Cal2AjZgO8iilxC`E8qJtP9f85*a!1B1rT8k4Ugl@5a zVDC`>5A_c{U1WpU)V=qXSlq}I&=0@`HY=qS|8rm;2Pr|yWqDTZmX7aPmb$LNsv{Zn z8SzGwG5$%(2Sm>jfa=)}3fbgv=d`07XR2Ux`80#`GqA|Q$hE(nJfgK3not*+LtHtp zkUUYBV^8@0iHI*;4i0IR{lJ*Ee%q?tYhC95-KNomoVr{QPE|rT1?yq?AV%(Y`+d3@ z(F0fXvR+a3ZcMtGznK8RiSXAzLa}Wr|BJr2k&v%GCeS!uJ{lSJ??8MD9BNRHU(e0> zqZpx*p(OHU=3K)DCA z=2^{w+d>GjKsW`q5Nx2M1G!iWDhP?eFSm4w0ZZ(pihBv#KJgl(!wMv&TY{lMOfZg3 zMer2=peO+uHI;(N`u`68l#6fovIH$ItD9Vj4Vvj!TFY@&>TnG~7ZA?WyDnL`7(7D) z-iuzI&(Kp?HuuT2xeSa*B^JmzLw5g!tTLjbqd~+R@<7W41WC@T}Wlrq3K$WHRQCXytGgfi z?~AWW(@qeZ80h&R?>+VC9vIJ6%b*ZBsc&O*i^R*1l!pu<%4|CCih{6Wmp^Qi&RgG9 zRcxko(as$x(WmHl-l~?`TY8=^2HtWx{&i<*;4-_KU`ev`;#YEZ!e`>!{Wr_?Uo1vL zwsij2lFayuWhjg1<0Sl){-aopK_znZg0T{vgn@)b#9<-xkepyJ)6Dz+j`;<1y7DA` zfQhN@MjE6s7Uo~5MK11%%m>p7{SPnYSbeFEnQ0~a=vJd33D-N4Ywpa8ot$RxRUD^+9!+&}#&40S?R32z3R>#PP54sUl zp?ge<{3tD99Fk-E{UT?X?7Yxm89JCneLZSKklWLhb%DZ1Sde9DW$_7iMjgHQLu{)6 zT@7z)jK@ygb7Roq#^YI+RG#^^$#hT^E;Hs~<**D<`yB9qk_dMqXsLu3BQ~w_xg#>?PRK>+H1uy7H?1Nb$ zaRNOcHs}lS;{EaDbA03oic9PGbE_WCqY(Uw*I?Z_#3)b={8x3lUQ|e=chBV~UN-4E z3}e}X0}gdzuj`>T;Ikb}V&hr$H+--D9Ae$JKvI@eDpfl_&BbpB4q)A-%Y0@4qY4Yb zgxo!87hhpp{e7?T2HB1d!m-=UCh%4^R4@w>{4UQ$?inc2{PX7QGf|L{GDI35uGjkW zV$N#9!gcBkIa!Lkx;f1tD!}^q+Ksgq#k`{k+M1rw!_YVqU8}g4&^|7QuTPLT(7G1O zNZZNe<)RH>U3-+)uf7y$q-8QcmP8;|S@{V}&Zg9v9XpE%&8c*LJ-$oNTiYoB4TgCs zw)7jEZ&Nt5^7Mk?JP=!(=dvpuC;#%aGg4 z463liqE+b$?Z|g%Oej+h2a;IP?(z`tT11PM(oWa{opixn8?Spp&2I_1&Kt{<#e5ycjX9_m-P? z;!%vKutf_sD+%B4C2!SvZ5j$u(|BXDVxJtC{x4gHEiIcGiN@l$yz-bib4n&`Dldoc zsd{z#RZ5*mVXgnwtbS&4&iW|UyW`M&;NPXzR`__a5Zk`k{IpXn=}d_aqO1BZ!KO1f zH>@o-=~mXa1@*1s1~)i2t#*<(xBFs+nKogCd^p0TK%Q}8F9>}rK}kF14jd;^yfh($ z`3Hgm@*+=zxw@6wdwasGU=f2~ut1EdN_CI=Lv@00*k=U3rY;)6ALoRJA z-k)txN-ItDwdU^|0ynb8mR-e-(+e4nPrS5d>m^;LoJjniURN9WBVAs*oGl7qFCWcp zGr=xj<%NixNM+U4<4?vsaRHBaWt851?b0gJ zTi3bYx7u81u`5>S!YSNoIL)F*Aoh9!8tcx`CI8JV2sUEu7a2*KK}T0+aYij; z{KFenb-E8lwm-A5c#O-r=qBApN%kkXm5weF>B&s)%9Jhnuv~4jm5#D<@c;UV4xAVD zLe>;SYvg@195$BtC&i@!F7ekw#78-t_a=7;GX~3RH{01C=8J#)(Rz^vsEP9V6ccjN zA-!^mKIf;{WncFpZspb(ig!rR#O*{A8Fs>c?qgj0>{qho3BH92Ni`s)#*t+|7=268 zWhANDPU?OeV!V<|b5DjAG%h6O5ip;eW5mQv!@cH{K}6zJ7Gac$Q;~wxwz5gP#i;Zh zPtwb7v(s%@nJW=*0n94y&$>JR^$wb6SF=^f39I5Q4``ghUFJuHD!-XZi^=9N%$fe3 z`X|irjzW!zKEk5A&;|Eul%5elqy;I!)tH~p2aK*Y$z~lKJ8;5H2wSUP5qIXcIWp0K znC;B_TX+J>mSdM2oU`dg>J(z1Ft z1#a94Nwo^-q|waO{T$&-D^WfFLmnI-Jn??}J>`C{!;4r(pRJ%TXG^t&n7$yxdNEX0 z$PfqYb1+jf6>-)83?olvVa4xJ_3(9PK66@Cg`)PjttPUXdlut}nHlb%{(X@6TXnIj z7a?un$l(@q0|6yf71hLV^dgt9k_;az1wsnJOmH z(S;YH@I`vW>8mecY2|X8xwE~$%HO5$Y*|@_ydy|MD3t-`pHTv#$ZUHb6O~bKb0IbQ z%*XC?MlPZ(VY$b)oXOM%N+<=R0Q8-kzI@Khaf$tmd}!*1g=7%er?2tVf#(@ky@BW; zP3s${)R8XI6vR)ElCc3^jSi}Urx53i{1u!HzenFI<@K3Jq%OzS4h<}vJUlhR!39TQ zXb7Rf9EY11FXSZW=Vf{1qc<1`+Ukz6IeS3laRYfs@dg=S#}Ba?eo|~h0ydeC#S+ku zJTWz=>WLh9R5e?D4+Eh;v+>6+l4Ta*$v+uqXrx$=3pj5#0 z8|kqx%K?@3nXR$l#Yi(2!(*7QfEg(|_(Ub?Jsd#K}Rylqvk7dii#d)S+Thpbtwz>x4AI zOi4%oVfGRT{pVj7E_?C8mzem|3Uq(PTBg5;kxO+gxt|?-^F6pB_$c(`j4Nj^E1VPF z9u``Qeh7nc5X&p6xqO_Gxgk0TbKJ|XI_x2Sa^(rl4Uwpl2-^)`bO_mRYC>~z6c^Ez zm&6M^btcl%D@G5Ls)Eu%w%Q#$g$|TO6Mm9EE_@9J7yaOR!ulie$eu~vVo6Phex)&y zTy9Ay5+uWQMxhv3mKE%l+bd+MDTDX^;3e(ZfbU8kf6NX?$Mh7bqUmdoG;GP%y^kFJdS7SeoOIPxB{%P5_fnKunEDA2=5ixUuL$ftA~%ok4;UcTM&`&Row znFt|XFE{O>vZ&vxE!Mm{p{;}NsGd-1g@#6<0)Frbduk=8)jM}(}K}oKajN~J`MB z=Bq-uA0&+mX&=Jsrf~3cIY8%b62x*03Y1q4g~=H%I^Smls#CwN+l5AA7u|3S`fEMAb3)kwDu49Yw?C`5*{%r!L{jD zQeB_6Zd4o79|~+(cpyabSn07H^5Zw6fd{GE5Ux{KUWxty?kV09nAz!u-|;GHbo`_R zcTBJo$2Y+Pk+Ck`3>+j_QXOHV5lH|hn5WH0%$3*+ep0+IHDzUF3|lx55_s|CVY@ow zfBJ|kf9LHK%(a{DU|AGU?ffD0CG`_ami+|EULW3-AW|Y;8Wf}r^BR*wt4iv)gAef# z%BbVA6?f{`-v}iSe5fU;^NI5z7S{>R&9Z_cB-J4o8L8oi-qa$6%&* z@j&KCJwvqV>}q1ci(ltN^zL&=8jY7(?^4uPNs&hy^ntv4o}G()sd(xeFKb!=ZUB$5 zvk%&}vuL$0b7-OmyfW7l5LEJ|Mw8bOPiP;?2K@asQ-W__Q=5x3nw;+B5Xa&DcOuR= z39(|qf5AtBN8b_xrzAV+TMHDrT##^~Oe%e)dX!qsGdtvq1^E29M%g*!pl*MMMjP<$ z1Rp^a%cmnbZvNh2Lo;Dh6sjz2*`GbernRL#C<|9n>nVfB=Sr5uU$K_%;>zhkL!-uQ z%|XC*buG3sss&;YlNKxi3Le%`^U=m$?t%QWxoLWyrqzvZrkjS$qwo|F>xmO)>bX_? z7PJXSPKW`1(MedVZTF2sWRz4`Vi;7cUK)UHmsWC43pX(i9Ss<$VG}>#Z;jdlCaB)H zsZYS6Inv>j)O;tSnNFZz8~(0jXf|OoA*HrdEj$F2Z)C-2fOXsO!oz98sIsS=F%e|_kMy8jm9K+phTp#bF8m%I5CXr28BZ1+2vx=wF%+p|LVf(1GI_VSMr5vF#z%#y zd916Q6Jap!zB&Dwn#OPHK2H-abl~(p-~~{fd~}yU7C_H5+ltMnwoz_(JDbzSX7Hf2 zJcGb-MjsiY{ac;O=h{w17LVl+KWp|C#nh=)`hUtBrDSIqS)ooU8@rG_{#tEm5yt|GVSRT3D*n%G^Tq)BbW&Ap*1g(dW#!L zY!0T%!=$?Aso!1HU$&g)<2@**Q#sw0D;k^0(l_t=HRCx~5--wg9%zHDQxdcLWdhIo zi-1HqihVgX$&<>W*IMV+IvpU-=HF*}u59t$`t9`c({I5Wgt?o`4K|uK=Y>ZhaD0e? zTignV-zkP-puP-H4KM1u*{@+`)f@EsCP%7V*%om<`DsobJKat%A5jK;3+jl+kx@s0 zj`#hAQ;eJ}6H?k9z95ez-FX)iQCgM-muC_}<3g?HVMqy1QC{1nQAmyU*?Ov3*}C{p z)Z~VUI}osC>@bC-UOjvHD-(=6=X>WE!3T$pvB4BZUL34BKkS>+!wV@C-XOe|Vo6tO z7(}5(T85hocN6rynZOE6hgteEz^aq3H#A1WNDjL*0kUt18zEx&zk*g5qln7>QV94U zXU9XDPou?gW6-}P@DY%aEG$OphCr<0JEUzx^Gq(Ke!fjfH^@GMjop3F?|p*xMzz2 zbBuf`l5yWCw(^7KQ?3E-ZVsjZVf&@AP3z7QI2lJNo`@IaeO~2GnHax2h#rG3me(dEeou1uHW+k;wqx4c7C{8 zxWy^h3oYs}@JD^5QBprDd6oasiZaJD+wjO6<6j>5-xDwg={)yGv1;L~d`@7#mp8rw z7!K|hTOVo7dY{&>dC#g))q5mEM&;T*n>LNdj3Mkb4icvsl+M#4X6gwn9WZ0mZSNAX zb*3Xf#t!iI-e^6_P*2~ar|tdP_kC*_K%X6FpqK%hLfENaraVg(^t7PLKY6#UU5M+FqFgIUL1kwc)rYR^z!dcIT^}%&Ik0)xEQB z3a@@d4>`R&S&i;ldZ)-xTjqg#&iT%ovb$dp^^Li}TUNIadQ%puAK_2JFa*aABTy$O zLcw6U??!&&=G&E)LeS0~njZO7N7?Lk$FhQ)kxp3Lq67kFF+_TgcuK9zA)CdNTYeW^ z)Z&I_l?VgO6z{G=zpgcUVKKE775Lb1ay-=jpt$!KzDT;?SRqi44TwuJ?D@NFvfgBE z(#vDd4>gmry%(=*an;e|VUAhI3xi&5`5&6meq~)=oy6stS;7V#R8P;n8C*J+MeL;+ z{&LcT5X0^e-;~)swF6plpR9e`U<5R$BSoY6lgY)ZZ^BJBe%7S1nn(rusG4UXLuLR- z5E@X^{JmV$f*v`Lgy&U% zojw#!4F8U;KGz_Q4eoym&m~Rrgd_CM(78C>=?}Z`G;}dFU0@vCMhtG1QIaHetB;HU zGm28RT=o8P-ilsLokM+#v{1Fiufn;*3I`&fBA1r$EWJs`V+UxgZ$$gE?B3+;)tet0 zQ4CuHbRs0I1g(nw^|!3MGC!d@ou4;6yTg?+F?!M)+s_8^&aIoq7}xubiRMCIv4QAevoiG-Hd@2%#WZbLkLT13t0 zdzKric-TFg<5hvLoQbXvi=LkL$qDO@A8yVk7g~aC5HxcqAHtP2bA^I3E%Ra=$sIiLFhLuh5{wnab zU$b*OLRtcTN!13zioA={R)&J8Np~YV32U^Ve}53-@4+ab^$_O^J<)DxwS8OK>5KXC z4`0S7L_{|NrnnsyLiB+^z^AB4O*}ja6taAZ_dj+J3CK0?;fD_E-`!FL$J@!q80&uu zWRCn`XF1K_Yo_#HP3DjvAIYPND^~+@;j!l{z@vm&_bCSk@)v>OZrw3yKh_PSKDSGl5sr*!Nh{M>ra#k+%9Cb&zG$bxWn&gO3w357@;gv7VuWqRZ**GLnA(q zITM9neT8i5)OVG<1dZ#3W7f6@y3puJ5k{ISLoUC2Rz!N~aP0XjZ)Kkj690)V8{OuE zK$Kmf^oETL$YuD7Dn<|z?msh&VEuvHmr!a;i{y!|kS6+GP|E1oF}C|-YH4OW31C&w z^ot~(oh}4L(socJA{!L4A1m<1%J-=vQUh{VO+5~a_-IK_=IXjm1DO;FjS|ks<-bjD z0hcMsfNFRmBd$jACyB~oUtRKmj)IOkdKC?}4+ghuA233S6HwLloH z2voM^1N-vhp>b0Wad`G70!H%8q->>oNt0kSSjrvuNX++91V0%`4vM%FrX;fG{Q~6s z^4~M-3%HD1_PjSe<3~?Lm(FrfTP(B`iiJK6SMD?z4xM@)_)}NSZiTh9_VUfS@KcuD z3p@315R*g_cz^kSygT#)2_u2hh)?<+28P}%lK~+m8c+d+pE8-;q*6M0{S951^t$6K zXsn(hP#$nVfL#+C8}iNO8!C?W2_<1?AsvpklbtzIH&P75_hO(f+JMHoW|f`j>+h3s z%A4}g(lZ*Or!d`P!Qnk>2@~Ci(R_JSs~O%^cx8!(6-5*=Qdpe9NS562Uk@a4DWDLh zD0x#&n6j+^S_DL_Tb-!8pIW=o4P0EYpvQw*4D_DNwda;I$dtkES_Mq}y8EJLZj?~gcd0h3&lgsr51L@p>`0?<~_RKOz>pE0B5 zC3je7xKbz5!vr>cs!5vu68BQjRMfwpYR^MDwWq}^>?Wy2))0EKV>!| zK`x5X$8VrD;`V3v7?tETe~xp>LS60}_rB&ysI2`zS$YQ!f(DwDe3a_5;fA!KmLjx+ zoqy(pSa&L|f66J>sIDxNxAAWZxwa*Keh5ImXnSm;0!-%}^dRn#+O*{CR&*47_BRXNM+zU3MmLSy|gE6?Z1~4&aGW=r!R7>mT@!{c}Yi0gq zsk}B~X^G*GBV9;QKVz-5sX6%!OmcSrh-zIXu8EJ&q%%kbky19%>eY*4ShATvJ;SK>y3d$mfU(~DY73_#N-l|5YvJmq}|dL|pz zs9Jvxy*__YGjV?4X|}O=9PZS%a{e6?<&*Sk9H>rYO|Ke?fI(&;owpv>%$WY8E~2g$ zlWa4zq096GHAfS|!fIqNvWTJLjW_n3uK#S;!||!Uoo=XsxHXaMvEv&P1Pfa?TjXk$ z5g8ezwSSwvx0(2S6H@3hg7Wa<8S`%7@dwn-lxV6M3UM9sK7MI#7KEF-l&vCf-4XS_ z@}xbqw4Y5)Q>YqbLCU7wbVnG$cTQ^E{{sDLJ}G;0e5wNX-w=JxXE9h7;4;Pvn@kO4 zK=(my*t{=s7F)Hk^xKhoh&{VMv{m)UoPSflq6Xvtse!gRtv(hbbr5Fw*zo1nc{KQ( zPU^D1rD1);MV2qRg&(L%lTjCY0%8^Mh!p1?Y9-e7_2m89R$5sCOR%nMHOxO9#b*%^ z1r^P_MKn*1UKL#QMb1}n`0V-ogi=nHi_0ZCvE{cbwaT!<2itHrlm=m=AS5+|_K3c8 z7)b^2lo{kA7*l;NJd8FTN@!K(Z?RXJ?9hlLq_C?6C=8uLIKqKot9vvoS$e{bW6aRU zg0G{zp*-=PlM*@5CJ|oSAnBNgcb{7|cw#fS2)%_{b?Z1@Z%TTa_N|Vtmd!v(dPW+d z2hJl)1I3#8Q6#r@){zmnyi{So2Ky_)>CmNCj|fUFMw4)DYo)u)=be6oRi(Cc z6qFCYPVmhYq{!}&MTlV)EYFMXM)?aG3bm7+;>fr8zPp_sj zx}VYxp4jdPGRKrwe&xd1F($!M&4RF4M!^SpU@X4VSl55pev);@m{^w(tE(#qz-Qr5=6Qq{iJo zb?ZVKR_ybPr@okyrHwJdr2s-c2G~(=aN0`348eoHO*3o=wZyD|ccrNPw+T3>XX7@&+PA$NF6{QX53lpZ2n{qfvvvU!S{u~!79eRnxX-2LRvb+iSPt|_-G`-(K{Z4ml@(;tz?;sj zYoRv3s*gQH>3H9dv~~*dRKF@_2!{xipJsZ)3iNXtmivjtq+Df;3|* zwfx%%p}Lu}27RQwZ$rMyueTi;06UCbt(C&&cCD#5%K&hMWT=K><^0GVaJx&ww#|ng zkW1`jCa-U!cQtrPTIx_oO5N;kLWnr=Bnr%AbVQE0fm2=D%6n$*g>(R6{bPI-;Y;%M zkXTyJrcX;bOitJg{|n8E(?B3pHkA=8am@!u4CJj|yW_!nJS}JGoMV5nSiaQrLgIQM zLDG|?;zW%wptbNr8Ib&i%W~oAZ;0+$1$zcL3|$_-+L+ReY_mSo$7%hVQCL_)mabu7 zAS}Nv8*eUN_Nl54_BHuoun+n)ImG#xM#t+Va((8M2Y67*eq?#Yxbq9YbeAQ|0A=1} z7!RHZV}$HqUBi%&=BJMoERXNgs1OQ?T(TP0kX3iioA?hKKB6m}w-@xa?K}t}P`mch z3kBu(1dkSg4tkM&=ER6RDS*7#bQGg~;ikkxkI zX(XfyNf*WB`&ht%U!ZIP>Lys4pl!WTh?xaS1l`C5PP2q?L)AqeS;FOdm6A`urQWn5 zyGh-xpTQ2gZ%*l)TgmKAL{{#Xv)5et0DcWgO zUi=E}jV@E>R7QL?J0${KE-{YpN0Yd;Cj5hh!`rmoKuilf4jYjii?tBIwCe-Fi*TOHhj=<@xoCSSdf+z)V zh;H6s_VZS`S*pY-2sc}A%dyUitx09U@%kg{K` zzp4WMVwFO)(05jPc!ta22W^Ac5kwSadQw#CG`Wr1w2HPQVQm>Z>{GRoj;uo6++QDR zWd8`YIz6Odis%_)X=_{aLJ{+$JeCGz8>s%pk1mi|4+gz^fXD&S;6HFUMLiDEOaV1R zPA!Eg@aXG#&kOn}`W)?u77Q+&}`>>CLch<&dA>J#=xVpcKQ!|w#sm<5Zu z(j%FLq;qX-eM1jc0IBtT@oWc0Am%RT-~qpz@bmpUJ{-iMkm@}c?7w74G=`21;RsbC zL@F=sKpag5V~}*4H)0RQ!FdSUlfbFFO8`jaLOI=V19XG=C|%)fpl`&i9|Fv(O&+{u z2!;>ZHnA3FXL3#l1|fC(DVK_~K1 0: - title = ", ".join([str(type.plural) for type in types]) + # types = args.getlist("type") + # types = [PackageType.get(tname) for tname in types] + # types = [type for type in types if type is not None] + # if len(types) > 0: + # title = ", ".join([str(type.plural) for type in types]) # Get tags types tags = args.getlist("tag") @@ -32,7 +32,7 @@ class QueryBuilder: self.hide_flags = set(args.getlist("hide")) self.title = title - self.types = types + # self.types = types self.tags = tags self.random = "random" in args @@ -126,8 +126,8 @@ class QueryBuilder: return query def filterPackageQuery(self, query): - if len(self.types) > 0: - query = query.filter(Package.type.in_(self.types)) + # if len(self.types) > 0: + # query = query.filter(Package.type.in_(self.types)) if self.author: author = User.query.filter_by(username=self.author).first() @@ -233,8 +233,8 @@ class QueryBuilder: query = query.filter(or_(ForumTopic.title.ilike('%' + self.search + '%'), ForumTopic.name == self.search.lower())) - if len(self.types) > 0: - query = query.filter(ForumTopic.type.in_(self.types)) + # if len(self.types) > 0: + # query = query.filter(ForumTopic.type.in_(self.types)) if self.limit: query = query.limit(self.limit) diff --git a/app/tasks/appstreamtasks.py b/app/tasks/appstreamtasks.py index e8dedc3..ebe6602 100644 --- a/app/tasks/appstreamtasks.py +++ b/app/tasks/appstreamtasks.py @@ -21,6 +21,16 @@ from app.utils import make_flask_login_password from app.utils.image import get_image_size from app.utils import randomString +map_categories = { + "tools": [ "development", "gtk", "qt" ], + "mods": [ "mod", "mods", "extension", "addon" ], + "games": [ "games", "game" ] +} + +exclude_hashtags = [ + "game" +] + #Workaround to get the urls because app.get_urls() doesn't work :| def get_urls(app): kinds = [AppStreamGlib.UrlKind(kind) for kind in range(11)] @@ -80,23 +90,39 @@ def importFromFlathub(): game1.state = PackageState.APPROVED game1.name = app.get_id() game1.title = app.get_name() - if "Development" in app.get_categories(): - game1.type = PackageType.TOOL - else: - game1.type = PackageType.GAME + hashtags = [] license = "Uknown" if app.get_project_license() is None else app.get_project_license().split("AND")[0].split("and")[0] if license not in licenses: row = License(license) licenses[row.name] = row session.add(row) session.commit() - for category in app.get_categories(): - if category.lower() not in tags: - row = Tag(category.lower()) - tags[row.name] = row - print("adding tag: ", row.name) - session.add(row) - game1.tags.append(tags[category.lower()]) + has_toplevel = False + categories = list(set([ x.lower() for x in app.get_categories() ])) + added = [] + # how do we get the type attribute from here? + # if app.get_type() == "addon": + # game1.tags.append(tags["mods"]) + # added.append("mods") + # has_toplevel = True + for category in categories: + if category in tags and category not in added: + if category in map_categories: + has_toplevel = True + game1.tags.append(tags[category]) + added.append(category) + elif category not in exclude_hashtags: + hashtags.append(category) + if not has_toplevel: + for map_category in map_categories: + if category in map_categories[map_category] and map_category not in added: + game1.tags.append(tags[map_category]) + added.append(map_category) + has_toplevel = True + break + if not has_toplevel and "games" not in added: + game1.tags.append(tags["games"]) + added.append("games") # this short list seems like a reasonable set of initial "featured" games if app.get_id() in alwaysAccept: @@ -110,12 +136,16 @@ def importFromFlathub(): for url,t in urls: if t == "bugtracker": game1.issueTracker = url - elif t == "homepage": + if "git" in url and "issues" in url: + game1.repo = url.replace("/issues", "") + elif t == "homepage" and not game1.repo: game1.repo = url + if t == "homepage" and "git" not in url: + game1.website = url game1.forums = 12835 game1.short_desc = "" or app.get_comment() - game1.desc = app.get_description() + game1.desc = app.get_description() + "\n " + ",".join([ "#" + x for x in hashtags ]) game1.install_desc = "Make sure to follow the [setup guide](https://flatpak.org/setup/) before installing. \n" game1.install_desc += f"\n```\nflatpak install flathub {app.get_id()}\n```\n" game1.install_desc += "Run: \n" diff --git a/app/tasks/forumtasks.py b/app/tasks/forumtasks.py index 4a5c322..132ce4c 100644 --- a/app/tasks/forumtasks.py +++ b/app/tasks/forumtasks.py @@ -117,76 +117,76 @@ def getLinksFromModSearch(): return links -@celery.task() -def importTopicList(): - links_by_id = getLinksFromModSearch() +# @celery.task() +# def importTopicList(): +# links_by_id = getLinksFromModSearch() - info_by_id = {} - getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': False }) - getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': True }) - getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False }) - getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True }) +# info_by_id = {} +# getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': False }) +# getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': True }) +# getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False }) +# getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True }) - # Caches - username_to_user = {} - topics_by_id = {} - for topic in ForumTopic.query.all(): - topics_by_id[topic.topic_id] = topic +# # Caches +# username_to_user = {} +# topics_by_id = {} +# for topic in ForumTopic.query.all(): +# topics_by_id[topic.topic_id] = topic - def get_or_create_user(username): - user = username_to_user.get(username) - if user: - return user +# def get_or_create_user(username): +# user = username_to_user.get(username) +# if user: +# return user - if not is_username_valid(username): - return None +# if not is_username_valid(username): +# return None - user = User.query.filter_by(forums_username=username).first() - if user is None: - user = User.query.filter_by(username=username).first() - if user: - return None +# user = User.query.filter_by(forums_username=username).first() +# if user is None: +# user = User.query.filter_by(username=username).first() +# if user: +# return None - user = User(username) - user.forums_username = username - db.session.add(user) +# user = User(username) +# user.forums_username = username +# db.session.add(user) - username_to_user[username] = user - return user +# username_to_user[username] = user +# return user - # Create or update - for info in info_by_id.values(): - id = int(info["id"]) +# # Create or update +# for info in info_by_id.values(): +# id = int(info["id"]) - # Get author - username = info["author"] - user = get_or_create_user(username) - if user is None: - print("Error! Unable to create user {}".format(username), file=sys.stderr) - continue +# # Get author +# username = info["author"] +# user = get_or_create_user(username) +# if user is None: +# print("Error! Unable to create user {}".format(username), file=sys.stderr) +# continue - # Get / add row - topic = topics_by_id.get(id) - if topic is None: - topic = ForumTopic() - db.session.add(topic) +# # Get / add row +# topic = topics_by_id.get(id) +# if topic is None: +# topic = ForumTopic() +# db.session.add(topic) - # Parse title - title, name = parseTitle(info["title"]) +# # Parse title +# title, name = parseTitle(info["title"]) - # Get link - link = links_by_id.get(id) +# # Get link +# link = links_by_id.get(id) - # Fill row - topic.topic_id = int(id) - topic.author = user - topic.type = info["type"] - topic.title = title - topic.name = name - topic.link = link - topic.wip = info["wip"] - topic.posts = int(info["posts"]) - topic.views = int(info["views"]) - topic.created_at = info["date"] +# # Fill row +# topic.topic_id = int(id) +# topic.author = user +# topic.type = info["type"] +# topic.title = title +# topic.name = name +# topic.link = link +# topic.wip = info["wip"] +# topic.posts = int(info["posts"]) +# topic.views = int(info["views"]) +# topic.created_at = info["date"] - db.session.commit() +# db.session.commit() diff --git a/app/tasks/importtasks.py b/app/tasks/importtasks.py index 0b2187a..2ae0fe1 100644 --- a/app/tasks/importtasks.py +++ b/app/tasks/importtasks.py @@ -75,8 +75,7 @@ def getMeta(urlstr, author): def postReleaseCheckUpdate(self, release: PackageRelease, path): try: - tree = build_tree(path, expected_type=ContentType[release.package.type.name], - author=release.package.author.username, name=release.package.name) + tree = build_tree(path, author=release.package.author.username, name=release.package.name) cache = {} @@ -109,9 +108,9 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path): db.session.add(Dependency(package, meta=meta, optional=True)) # Update game supports - if package.type == PackageType.TOOL: - resolver = GameSupportResolver() - resolver.update(package) + # if package.type == PackageType.TOOL: + # resolver = GameSupportResolver() + # resolver.update(package) # # Update min/max # if tree.meta.get("min_minetest_version"): diff --git a/app/templates/base.html b/app/templates/base.html index 3be58ec..2eb3a16 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -6,32 +6,29 @@ {% block title %}title{% endblock %} - {{ config.USER_APP_NAME }} - + - - - + {% block headextra %}{% endblock %}