tag-based navigation implemented, added in-line screenshot viewer, fixes #10

This commit is contained in:
Armen 2022-03-15 20:55:39 -04:00
parent 8df6ebd2e7
commit 21fe900cc8
57 changed files with 624 additions and 645 deletions

View File

@ -26,8 +26,8 @@ from sqlalchemy import or_, and_
from app.logic.game_support import GameSupportResolver from app.logic.game_support import GameSupportResolver
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \ from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, PackageGameSupport NotificationType, PackageUpdateConfig, License, UserRank, PackageGameSupport
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts # from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates
from app.tasks.appstreamtasks import importFromFlathub from app.tasks.appstreamtasks import importFromFlathub
from app.utils import addNotification, get_system_user from app.utils import addNotification, get_system_user
@ -90,20 +90,20 @@ def reimport_packages():
return redirect(url_for("todo.view_editor")) return redirect(url_for("todo.view_editor"))
@action("Import forum topic list") # @action("Import forum topic list")
def import_topic_list(): # def import_topic_list():
task = importTopicList.delay() # task = importTopicList.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page"))) # return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
@action("Import appstream from flathub") @action("Import appstream from flathub")
def import_from_flathub(): def import_from_flathub():
task = importFromFlathub.delay() task = importFromFlathub.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics"))) return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics")))
@action("Check all forum accounts") # @action("Check all forum accounts")
def check_all_forum_accounts(): # def check_all_forum_accounts():
task = checkAllForumAccounts.delay() # task = checkAllForumAccounts.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page"))) # return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
@action("Import screenshots") @action("Import screenshots")
@ -297,13 +297,12 @@ def delete_inactive_users():
@action("Send Video URL notification") @action("Send Video URL notification")
def remind_video_url(): def remind_video_url():
users = User.query.filter(User.maintained_packages.any( 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() system_user = get_system_user()
for user in users: for user in users:
packages = db.session.query(Package.title).filter( packages = db.session.query(Package.title).filter(
or_(Package.author==user, Package.maintainers.any(User.id==user.id)), or_(Package.author==user, Package.maintainers.any(User.id==user.id)),
Package.video_url.is_(None), Package.video_url.is_(None),
Package.type == PackageType.GAME,
Package.state == PackageState.APPROVED) \ Package.state == PackageState.APPROVED) \
.all() .all()

View File

@ -25,10 +25,10 @@ from sqlalchemy.sql.expression import func
from app import csrf from app import csrf
from app.markdown import render_markdown 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 APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread
from app.querybuilder import QueryBuilder 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 . import bp
from .auth import is_api_authd from .auth import is_api_authd
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \ 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 = [] ret = []
out[id] = ret out[id] = ret
if package.type != PackageType.TOOL:
return
for dep in package.dependencies: for dep in package.dependencies:
if only_hard and dep.optional: if only_hard and dep.optional:
continue 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] fulfilled_by = [ pkg.getId() for pkg in dep.meta_package.packages]
if depth == 1 and not dep.optional: 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: if most_likely:
resolve_package_deps(out, most_likely, only_hard, depth + 1) 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( featured = query.filter(Package.tags.any(name="featured")).order_by(
func.random()).limit(6).all() func.random()).limit(6).all()
new = query.order_by(db.desc(Package.approved_at)).limit(4).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() toplevel_tags = get_toplevel_tags()
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(8).all() popular = {}
pop_txp = query.filter_by(type=PackageType.ASSETPACK).order_by(db.desc(Package.score)).limit(8).all() # 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)) \ high_reviewed = query.order_by(db.desc(Package.score - Package.score_downloads)) \
.filter(Package.reviews.any()).limit(4).all() .filter(Package.reviews.any()).limit(4).all()
@ -487,16 +486,15 @@ def homepage():
def mapPackages(packages: List[Package]): def mapPackages(packages: List[Package]):
return [pkg.getAsDictionaryShort(current_app.config["BASE_URL"]) for pkg in packages] return [pkg.getAsDictionaryShort(current_app.config["BASE_URL"]) for pkg in packages]
popular = { k:mapPackages(v) for (k, v) in popular.items() }
return jsonify({ return jsonify({
"count": count, "count": count,
"downloads": downloads, "downloads": downloads,
"featured": mapPackages(featured), "featured": mapPackages(featured),
"new": mapPackages(new), "new": mapPackages(new),
"updated": mapPackages(updated), "updated": mapPackages(updated),
"pop_mod": mapPackages(pop_mod), "popular": popular,
"pop_txp": mapPackages(pop_txp), "toplevel": [ x.getAsDictionary() for x in toplevel_tags ],
"pop_game": mapPackages(pop_gam),
"high_reviewed": mapPackages(high_reviewed) "high_reviewed": mapPackages(high_reviewed)
}) })
@ -505,7 +503,7 @@ def homepage():
@cors_allowed @cors_allowed
def welcome_v1(): def welcome_v1():
featured = Package.query \ featured = Package.query \
.filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED, .filter(Package.state == PackageState.APPROVED,
Package.tags.any(name="featured")) \ Package.tags.any(name="featured")) \
.order_by(func.random()) \ .order_by(func.random()) \
.limit(5).all() .limit(5).all()
@ -520,23 +518,6 @@ def welcome_v1():
"featured": map_packages(featured), "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/") @bp.route("/api/dependencies/")
@cors_allowed @cors_allowed
def all_deps(): def all_deps():
@ -545,7 +526,7 @@ def all_deps():
def format_pkg(pkg: Package): def format_pkg(pkg: Package):
return { return {
"type": pkg.type.toName(), # "type": pkg.type.toName(),
"author": pkg.author.username, "author": pkg.author.username,
"name": pkg.name, "name": pkg.name,
"provides": [x.name for x in pkg.provides], "provides": [x.name for x in pkg.provides],

View File

@ -3,6 +3,7 @@ from flask import Blueprint, render_template, redirect
bp = Blueprint("homepage", __name__) bp = Blueprint("homepage", __name__)
from app.models import * from app.models import *
from app.utils import get_toplevel_tags
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
@ -18,11 +19,12 @@ def home():
count = query.count() count = query.count()
featured = query.filter(Package.tags.any(name="featured")).order_by(func.random()).limit(6).all() 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() 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() for toplevel_tag in toplevel_tags:
pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all() popular[toplevel_tag.name] = join(query.filter(Package.tags.any(name=toplevel_tag.name)).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()
high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \ high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \
.filter(Package.reviews.any()).limit(4).all() .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() .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, 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)

View File

@ -18,16 +18,13 @@ from flask import render_template, abort
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from . import bp from . import bp
from app.utils import is_package_page from app.utils import is_package_page, get_toplevel_tags
from ...models import Package, PackageType, PackageState, db, PackageRelease from ...models import Package, PackageState, db, PackageRelease
@bp.route("/packages/<author>/<name>/hub/") @bp.route("/packages/<author>/<name>/hub/")
@is_package_page @is_package_page
def game_hub(package: Package): def game_hub(package: Package):
if package.type != PackageType.GAME:
abort(404)
def join(query): def join(query):
return query.options( return query.options(
joinedload(Package.license), joinedload(Package.license),
@ -37,9 +34,11 @@ def game_hub(package: Package):
count = query.count() count = query.count()
new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all() 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() toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).order_by(db.desc(Tag.id)).all()
pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all() popular = {}
pop_txp = join(query.filter_by(type=PackageType.ASSETPACK).order_by(db.desc(Package.score))).limit(8).all() 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))) \ high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \
.filter(Package.reviews.any()).limit(4).all() .filter(Package.reviews.any()).limit(4).all()
@ -50,5 +49,5 @@ def game_hub(package: Package):
updated = updated[:4] updated = updated[:4]
return render_template("packages/game_hub.html", package=package, count=count, 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) high_reviewed=high_reviewed)

View File

@ -99,10 +99,12 @@ def list_all():
selected_tags = set(qb.tags) selected_tags = set(qb.tags)
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
return render_template("packages/list.html", return render_template("packages/list.html",
query_hint=title, packages=query.items, pagination=query, query_hint=title, packages=query.items, pagination=query,
query=search, tags=tags, selected_tags=selected_tags, type=type_name, 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): def getReleases(package):
@ -120,7 +122,7 @@ def view(package):
package.checkPerm(current_user, Permission.APPROVE_NEW)) package.checkPerm(current_user, Permission.APPROVE_NEW))
conflicting_modnames = None conflicting_modnames = None
if show_similar and package.type != PackageType.ASSETPACK: if show_similar:
conflicting_modnames = db.session.query(MetaPackage.name) \ conflicting_modnames = db.session.query(MetaPackage.name) \
.filter(MetaPackage.id.in_([ mp.id for mp in package.provides ])) \ .filter(MetaPackage.id.in_([ mp.id for mp in package.provides ])) \
.filter(MetaPackage.packages.any(Package.id != package.id)) \ .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]) conflicting_modnames = set([x[0] for x in conflicting_modnames])
packages_uses = None packages_uses = None
if package.type == PackageType.TOOL: packages_uses = Package.query.filter(
packages_uses = Package.query.filter( Package.id != package.id,
Package.type == PackageType.TOOL, Package.state == PackageState.APPROVED,
Package.id != package.id, Package.dependencies.any(
Package.state == PackageState.APPROVED, Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
Package.dependencies.any( .order_by(db.desc(Package.score)).limit(6).all()
Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
.order_by(db.desc(Package.score)).limit(6).all()
releases = getReleases(package) 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 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", return render_template("packages/view.html",
package=package, releases=releases, packages_uses=packages_uses, package=package, releases=releases, packages_uses=packages_uses,
conflicting_modnames=conflicting_modnames, conflicting_modnames=conflicting_modnames,
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, 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/<author>/<name>/shields/<type>/") @bp.route("/packages/<author>/<name>/shields/<type>/")
@ -226,7 +228,14 @@ def makeLabel(obj):
class PackageForm(FlaskForm): 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)]) 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"))]) 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)]) 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.tags.data = package.tags
form.content_warnings.data = package.content_warnings 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(): if form.validate_on_submit():
wasNew = False wasNew = False
if not package: if not package:
@ -353,7 +359,7 @@ def create_edit(author=None, name=None):
form=form, author=author, enable_wizard=enableWizard, form=form, author=author, enable_wizard=enableWizard,
packages=package_query.all(), packages=package_query.all(),
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).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/<author>/<name>/state/", methods=["POST"]) @bp.route("/packages/<author>/<name>/state/", methods=["POST"])
@ -408,7 +414,7 @@ def move_to_state(package):
def remove(package): def remove(package):
if request.method == "GET": if request.method == "GET":
return render_template("packages/remove.html", package=package, 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 "?" 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() 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, 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/<author>/<name>/remove-self-maintainer/", methods=["POST"]) @bp.route("/packages/<author>/<name>/remove-self-maintainer/", methods=["POST"])
@ -540,7 +546,7 @@ def audit(package):
pagination = query.paginate(page, num, True) pagination = query.paginate(page, num, True)
return render_template("packages/audit.html", log=pagination.items, pagination=pagination, 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): class PackageAliasForm(FlaskForm):
@ -554,7 +560,7 @@ class PackageAliasForm(FlaskForm):
@rank_required(UserRank.EDITOR) @rank_required(UserRank.EDITOR)
@is_package_page @is_package_page
def alias_list(package: Package): 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/<author>/<name>/aliases/new/", methods=["GET", "POST"]) @bp.route("/packages/<author>/<name>/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 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/<author>/<name>/share/") @bp.route("/packages/<author>/<name>/share/")
@ -588,7 +594,7 @@ def alias_create_edit(package: Package, alias_id: int = None):
@is_package_page @is_package_page
def share(package): def share(package):
return render_template("packages/share.html", package=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/<author>/<name>/similar/") @bp.route("/packages/<author>/<name>/similar/")
@ -610,4 +616,4 @@ def similar(package):
# .all() # .all()
return render_template("packages/similar.html", package=package, return render_template("packages/similar.html", package=package,
packages_modnames=packages_modnames, similar_topics=[]) packages_modnames=packages_modnames, similar_topics=[], toplevel=get_toplevel_tags())

View File

@ -35,7 +35,7 @@ from . import bp, get_package_tabs
def list_releases(package): def list_releases(package):
return render_template("packages/releases_list.html", return render_template("packages/releases_list.html",
package=package, 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): # def get_mt_releases(is_max):
@ -92,7 +92,7 @@ def create_release(package):
except LogicError as e: except LogicError as e:
flash(e.message, "danger") 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/<author>/<name>/releases/<id>/download/") @bp.route("/packages/<author>/<name>/releases/<id>/download/")
@ -163,7 +163,7 @@ def edit_release(package, id):
db.session.commit() db.session.commit()
return redirect(package.getURL("packages.list_releases")) 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 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/<author>/<name>/releases/<id>/delete/", methods=["POST"]) @bp.route("/packages/<author>/<name>/releases/<id>/delete/", methods=["POST"])
@ -301,7 +301,7 @@ def update_config(package):
return redirect(package.getURL("packages.list_releases")) 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/<author>/<name>/setup-releases/") @bp.route("/packages/<author>/<name>/setup-releases/")
@ -314,7 +314,7 @@ def setup_releases(package):
if package.update_config: if package.update_config:
return redirect(package.getURL("packages.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/") @bp.route("/user/update-configs/")
@ -343,4 +343,4 @@ def bulk_update_config(username=None):
Package.update_config.has()) \ Package.update_config.has()) \
.order_by(db.asc(Package.title)).all() .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())

View File

@ -26,7 +26,7 @@ from wtforms import *
from wtforms.validators import * from wtforms.validators import *
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \ from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \
Permission, AuditSeverity 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 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)) 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) 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): class ReviewForm(FlaskForm):
@ -123,7 +123,7 @@ def review(package):
return redirect(package.getURL("packages.view")) return redirect(package.getURL("packages.view"))
return render_template("packages/review_create_edit.html", 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/<author>/<name>/reviews/<reviewer>/delete/", methods=["POST"]) @bp.route("/packages/<author>/<name>/reviews/<reviewer>/delete/", methods=["POST"])
@ -237,4 +237,4 @@ def review_votes(package):
user_biases_info.sort(key=lambda x: -abs(x.balance)) user_biases_info.sort(key=lambda x: -abs(x.balance))
return render_template("packages/review_votes.html", form=form, package=package, reviews=package.reviews, 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())

View File

@ -73,7 +73,7 @@ def screenshots(package):
db.session.commit() db.session.commit()
return render_template("packages/screenshots.html", package=package, form=form, 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/<author>/<name>/screenshots/new/", methods=["GET", "POST"]) @bp.route("/packages/<author>/<name>/screenshots/new/", methods=["GET", "POST"])
@ -92,7 +92,7 @@ def create_screenshot(package):
except LogicError as e: except LogicError as e:
flash(e.message, "danger") 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/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"]) @bp.route("/packages/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
@ -124,7 +124,7 @@ def edit_screenshot(package, id):
db.session.commit() db.session.commit()
return redirect(package.getURL("packages.screenshots")) 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/<author>/<name>/screenshots/<id>/delete/", methods=["POST"]) @bp.route("/packages/<author>/<name>/screenshots/<id>/delete/", methods=["POST"])

View File

@ -15,7 +15,9 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import base64
import string
import random
from flask import * from flask import *
from flask_babel import gettext, lazy_gettext, get_locale from flask_babel import gettext, lazy_gettext, get_locale
from flask_login import current_user, login_required, logout_user, login_user 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 sqlalchemy import or_
from wtforms import * from wtforms import *
from wtforms.validators import * from wtforms.validators import *
from captcha.image import ImageCaptcha
from app.models import * from app.models import *
from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email 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"))]) Regexp("^[a-zA-Z0-9._-]+$", message=lazy_gettext("Only a-zA-Z0-9._ allowed"))])
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()]) email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(6, 100)]) 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()]) agree = BooleanField(lazy_gettext("I agree"), [DataRequired()])
submit = SubmitField(lazy_gettext("Register")) submit = SubmitField(lazy_gettext("Register"))
def handle_register(form): 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") flash(gettext("Incorrect captcha answer"), "danger")
return return
@ -181,7 +184,13 @@ def register():
if ret: if ret:
return 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")) suggested_password=genphrase(entropy=52, wordset="bip39"))

View File

@ -151,24 +151,17 @@ def get_user_medals(user: User) -> Tuple[List[Medal], List[Medal]]:
.filter(text("rank <= 30")) \ .filter(text("rank <= 30")) \
.all() .all()
user_package_ranks = next( user_package_ranks = next((x for x in user_package_ranks if x[2] <= 10), None)
(x for x in user_package_ranks if x[0] == PackageType.TOOL or x[2] <= 10),
None)
if user_package_ranks: if user_package_ranks:
top_rank = user_package_ranks[2] 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: 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: else:
title = gettext(u"Top %(group)d %(type)s", group=top_rank, type=top_type.text.lower()) title = gettext(u"Top %(group)d projects", group=top_rank, type=top_type.text.lower())
if top_type == PackageType.TOOL: icon = "fa-paint-brush"
icon = "fa-box"
elif top_type == PackageType.GAME:
icon = "fa-gamepad"
else:
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) display_name=user.display_name, type=top_type.text.lower(), place=top_rank)
unlocked.append( unlocked.append(
Medal.make_unlocked(place_to_color(top_rank), icon, title, description)) Medal.make_unlocked(place_to_color(top_rank), icon, title, description))

View File

@ -36,11 +36,11 @@ def populate(session):
featured.is_protected = True featured.is_protected = True
# These tags replace "package types" # These tags replace "package types"
game_tag = Tag("Game") game_tag = Tag("Games")
game_tag.is_toplevel = True game_tag.is_toplevel = True
tool_tag = Tag("Tool") tool_tag = Tag("Tools")
tool_tag.is_toplevel = True tool_tag.is_toplevel = True
mod_tag = Tag("Mod") mod_tag = Tag("Mods")
mod_tag.is_toplevel = True mod_tag.is_toplevel = True
session.add(featured) session.add(featured)
session.add(game_tag) session.add(game_tag)
@ -110,7 +110,7 @@ def populate_test_data(session):
tool.title = "Alpha Test" tool.title = "Alpha Test"
tool.license = licenses["MIT"] tool.license = licenses["MIT"]
tool.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
tool.type = PackageType.TOOL # tool.type = PackageType.TOOL
tool.author = admin_user tool.author = admin_user
tool.tags.append(tags["mapgen"]) tool.tags.append(tags["mapgen"])
tool.tags.append(tags["environment"]) tool.tags.append(tags["environment"])
@ -134,7 +134,7 @@ def populate_test_data(session):
mod1.title = "Awards" mod1.title = "Awards"
mod1.license = licenses["LGPLv2.1"] mod1.license = licenses["LGPLv2.1"]
mod1.media_license = licenses["MIT"] mod1.media_license = licenses["MIT"]
mod1.type = PackageType.TOOL # mod1.type = PackageType.TOOL
mod1.author = admin_user mod1.author = admin_user
mod1.tags.append(tags["player_effects"]) mod1.tags.append(tags["player_effects"])
mod1.repo = "https://github.com/libregaming/awards" mod1.repo = "https://github.com/libregaming/awards"
@ -170,7 +170,7 @@ awards.register_achievement("award_mesefind",{
mod2.name = "mesecons" mod2.name = "mesecons"
mod2.title = "Mesecons" mod2.title = "Mesecons"
mod2.tags.append(tags["tools"]) mod2.tags.append(tags["tools"])
mod2.type = PackageType.TOOL # mod2.type = PackageType.TOOL
mod2.license = licenses["LGPLv3"] mod2.license = licenses["LGPLv3"]
mod2.media_license = licenses["MIT"] mod2.media_license = licenses["MIT"]
mod2.author = jeija mod2.author = jeija
@ -260,7 +260,7 @@ No warranty is provided, express or implied, for any part of the project.
tool.title = "Handholds" tool.title = "Handholds"
tool.license = licenses["MIT"] tool.license = licenses["MIT"]
tool.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
tool.type = PackageType.TOOL # tool.type = PackageType.TOOL
tool.author = ez tool.author = ez
tool.tags.append(tags["player_effects"]) tool.tags.append(tags["player_effects"])
tool.repo = "https://github.com/ezhh/handholds" 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.title = "Other Worlds"
tool.license = licenses["MIT"] tool.license = licenses["MIT"]
tool.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
tool.type = PackageType.TOOL # tool.type = PackageType.TOOL
tool.author = ez tool.author = ez
tool.tags.append(tags["mapgen"]) tool.tags.append(tags["mapgen"])
tool.tags.append(tags["environment"]) 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.title = "Food"
tool.license = licenses["LGPLv2.1"] tool.license = licenses["LGPLv2.1"]
tool.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
tool.type = PackageType.TOOL # tool.type = PackageType.TOOL
tool.author = admin_user tool.author = admin_user
tool.tags.append(tags["player_effects"]) tool.tags.append(tags["player_effects"])
tool.repo = "https://github.com/libregaming/food/" 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.title = "Sweet Foods"
tool.license = licenses["CC0"] tool.license = licenses["CC0"]
tool.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
tool.type = PackageType.TOOL # tool.type = PackageType.TOOL
tool.author = admin_user tool.author = admin_user
tool.tags.append(tags["player_effects"]) tool.tags.append(tags["player_effects"])
tool.repo = "https://github.com/libregaming/food_sweet/" 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.state = PackageState.APPROVED
game1.name = "capturetheflag" game1.name = "capturetheflag"
game1.title = "Capture The Flag" game1.title = "Capture The Flag"
game1.type = PackageType.GAME # game1.type = PackageType.GAME
game1.license = licenses["LGPLv2.1"] game1.license = licenses["LGPLv2.1"]
game1.media_license = licenses["MIT"] game1.media_license = licenses["MIT"]
game1.author = admin_user game1.author = admin_user
@ -397,7 +397,7 @@ Uses the CTF PvP Engine.
tool.title = "PixelBOX Reloaded" tool.title = "PixelBOX Reloaded"
tool.license = licenses["CC0"] tool.license = licenses["CC0"]
tool.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
tool.type = PackageType.ASSETPACK # tool.type = PackageType.ASSETPACK
tool.author = admin_user tool.author = admin_user
tool.forums = 14132 tool.forums = 14132
tool.short_desc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit" 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() session.commit()
metas = {} # metas = {}
for package in Package.query.filter_by(type=PackageType.TOOL).all(): # for package in Package.query.filter_by(type=PackageType.TOOL).all():
meta = None # meta = None
try: # try:
meta = metas[package.name] # meta = metas[package.name]
except KeyError: # except KeyError:
meta = MetaPackage(package.name) # meta = MetaPackage(package.name)
session.add(meta) # session.add(meta)
metas[package.name] = meta # metas[package.name] = meta
package.provides.append(meta) # package.provides.append(meta)
dep = Dependency(food_sweet, meta=metas["food"]) # dep = Dependency(food_sweet, meta=metas["food"])
session.add(dep) # session.add(dep)

View File

@ -390,9 +390,8 @@ Supported query parameters:
* `downloads`: get number of downloads * `downloads`: get number of downloads
* `new`: new packages * `new`: new packages
* `updated`: recently updated packages * `updated`: recently updated packages
* `pop_mod`: popular mods * `popular`: popular packages
* `pop_txp`: popular textures * `toplevel`: toplevel nav tags
* `pop_game`: popular games
* `high_reviewed`: highest reviewed * `high_reviewed`: highest reviewed
* GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning) * GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning)
* `featured`: featured games * `featured`: featured games

View File

@ -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. GitHub can only be used to login, not to register.
<a class="btn btn-primary" href="/user/claim/">Register</a> <a class="btn btn-primary" href="/user/register/">Register</a>
### My verification email never arrived ### My verification email never arrived

View File

@ -20,7 +20,7 @@ import sys
from typing import List, Dict, Optional, Iterator, Iterable from typing import List, Dict, Optional, Iterator, Iterable
from app.logic.LogicError import LogicError 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): get_game_support(package):
@ -128,39 +128,36 @@ class GameSupportResolver:
history = history.copy() history = history.copy()
history.append(key) history.append(key)
if package.type == PackageType.GAME: # if package.type == PackageType.GAME:
return PackageSet([package]) return PackageSet([package])
if key in self.resolved_packages: # if key in self.resolved_packages:
return self.resolved_packages.get(key) # return self.resolved_packages.get(key)
if key in self.checked_packages: # if key in self.checked_packages:
print(f"Error, cycle found: {','.join(history)}", file=sys.stderr) # print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
return PackageSet() # return PackageSet()
self.checked_packages.add(key) # self.checked_packages.add(key)
if package.type != PackageType.TOOL: # retval = PackageSet()
raise LogicError(500, "Got non-tool")
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(): # self.resolved_packages[key] = retval
ret = self.resolve_for_meta_package(dep.meta_package, history) # return retval
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
def update_all(self) -> None: 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, []) retval = self.resolve(package, [])
for game in retval: for game in retval:
support = PackageGameSupport(package, game) support = PackageGameSupport(package, game)

View File

@ -20,7 +20,7 @@ import validators
from flask_babel import lazy_gettext from flask_babel import lazy_gettext
from app.logic.LogicError import LogicError 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 License, UserRank, PackageDevState
from app.utils import addAuditLog from app.utils import addAuditLog
from app.utils.url import clean_youtube_url 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) validate(data)
if "type" in data: # if "type" in data:
data["type"] = PackageType.coerce(data["type"]) # data["type"] = PackageType.coerce(data["type"])
if "dev_state" in data: if "dev_state" in data:
data["dev_state"] = PackageDevState.coerce(data["dev_state"]) 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: if key in data:
setattr(package, key, data[key]) setattr(package, key, data[key])
if package.type == PackageType.ASSETPACK: # if package.type == PackageType.ASSETPACK:
package.license = package.media_license # package.license = package.media_license
if was_new and package.type == PackageType.TOOL: # if was_new and package.type == PackageType.TOOL:
m = MetaPackage.GetOrCreate(package.name, {}) # m = MetaPackage.GetOrCreate(package.name, {})
package.provides.append(m) # package.provides.append(m)
if "tags" in data: if "tags" in data:
old_tags = list(package.tags) old_tags = list(package.tags)

View File

@ -120,7 +120,7 @@ class ForumTopic(db.Model):
wip = db.Column(db.Boolean, default=False, nullable=False) wip = db.Column(db.Boolean, default=False, nullable=False)
discarded = 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) title = db.Column(db.String(200), nullable=False)
name = db.Column(db.String(30), nullable=True) name = db.Column(db.String(30), nullable=True)
link = db.Column(db.String(200), nullable=True) link = db.Column(db.String(200), nullable=True)

View File

@ -48,49 +48,49 @@ class License(db.Model):
return self.name return self.name
class PackageType(enum.Enum): # class PackageType(enum.Enum):
GAME = "Game" # GAME = "Game"
TOOL = "Tool" # TOOL = "Tool"
ASSETPACK = "Asset Pack" # ASSETPACK = "Asset Pack"
def toName(self): # def toName(self):
return self.name.lower() # return self.name.lower()
def __str__(self): # def __str__(self):
return self.name # return self.name
@property # @property
def text(self): # def text(self):
if self == PackageType.TOOL: # if self == PackageType.TOOL:
return lazy_gettext("Tool") # return lazy_gettext("Tool")
elif self == PackageType.GAME: # elif self == PackageType.GAME:
return lazy_gettext("Game") # return lazy_gettext("Game")
elif self == PackageType.ASSETPACK: # elif self == PackageType.ASSETPACK:
return lazy_gettext("Asset Pack") # return lazy_gettext("Asset Pack")
@property # @property
def plural(self): # def plural(self):
if self == PackageType.TOOL: # if self == PackageType.TOOL:
return lazy_gettext("Tools") # return lazy_gettext("Tools")
elif self == PackageType.GAME: # elif self == PackageType.GAME:
return lazy_gettext("Games") # return lazy_gettext("Games")
elif self == PackageType.ASSETPACK: # elif self == PackageType.ASSETPACK:
return lazy_gettext("Asset Packs") # return lazy_gettext("Asset Packs")
@classmethod # @classmethod
def get(cls, name): # def get(cls, name):
try: # try:
return PackageType[name.upper()] # return PackageType[name.upper()]
except KeyError: # except KeyError:
return None # return None
@classmethod # @classmethod
def choices(cls): # def choices(cls):
return [(choice, choice.text) for choice in cls] # return [(choice, choice.text) for choice in cls]
@classmethod # @classmethod
def coerce(cls, item): # def coerce(cls, item):
return item if type(item) == PackageType else PackageType[item.upper()] # return item if type(item) == PackageType else PackageType[item.upper()]
class PackageDevState(enum.Enum): class PackageDevState(enum.Enum):
@ -218,7 +218,7 @@ class PackagePropertyKey(enum.Enum):
title = "Title" title = "Title"
short_desc = "Short Description" short_desc = "Short Description"
desc = "Description" desc = "Description"
type = "Type" # type = "Type"
license = "License" license = "License"
media_license = "Media License" media_license = "Media License"
tags = "Tags" tags = "Tags"
@ -378,7 +378,7 @@ class Package(db.Model):
desc = db.Column(db.UnicodeText, nullable=True) desc = db.Column(db.UnicodeText, nullable=True)
build_desc = db.Column(db.UnicodeText, nullable=True) build_desc = db.Column(db.UnicodeText, nullable=True)
install_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) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
approved_at = db.Column(db.DateTime, nullable=True, default=None) approved_at = db.Column(db.DateTime, nullable=True, default=None)
@ -534,7 +534,7 @@ class Package(db.Model):
"title": self.title, "title": self.title,
"author": self.author.username, "author": self.author.username,
"short_description": short_desc, "short_description": short_desc,
"type": self.type.toName(), # "type": self.type.toName(),
"release": release_id, "release": release_id,
"thumbnail": (base_url + tnurl) if tnurl is not None else None, "thumbnail": (base_url + tnurl) if tnurl is not None else None,
"aliases": [ alias.getAsDictionary() for alias in self.aliases ], "aliases": [ alias.getAsDictionary() for alias in self.aliases ],
@ -559,7 +559,7 @@ class Package(db.Model):
"title": self.title, "title": self.title,
"short_description": self.short_desc, "short_description": self.short_desc,
"long_description": self.desc, "long_description": self.desc,
"type": self.type.toName(), # "type": self.type.toName(),
"created_at": self.created_at.isoformat(), "created_at": self.created_at.isoformat(),
"license": self.license.name, "license": self.license.name,
@ -771,7 +771,7 @@ class MetaPackage(db.Model):
dependencies = db.relationship("Dependency", back_populates="meta_package", lazy="dynamic") dependencies = db.relationship("Dependency", back_populates="meta_package", lazy="dynamic")
packages = db.relationship("Package", lazy="dynamic", back_populates="provides", secondary=PackageProvides) 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): def __init__(self, name=None):
self.name = name 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): class PackageRelease(db.Model):
id = db.Column(db.Integer, primary_key=True) 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) downloads = db.Column(db.Integer, nullable=False, default=0)
channel = db.Column(db.String(200), nullable=False, default="") 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 # 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)") 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, "commit": self.commit_hash,
"downloads": self.downloads, "downloads": self.downloads,
"channel": self.channel, "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): def getLongAsDictionary(self):
@ -963,8 +914,6 @@ class PackageRelease(db.Model):
"release_date": self.releaseDate.isoformat(), "release_date": self.releaseDate.isoformat(),
"commit": self.commit_hash, "commit": self.commit_hash,
"downloads": self.downloads, "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, "channel": self.channel,
"package": self.package.getAsDictionaryKey() "package": self.package.getAsDictionaryKey()
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -3,7 +3,7 @@ from sqlalchemy import or_
from sqlalchemy.orm import subqueryload from sqlalchemy.orm import subqueryload
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from .models import db, PackageType, Package, ForumTopic, License, PackageRelease, User, Tag, \ from .models import db, Package, ForumTopic, License, PackageRelease, User, Tag, \
ContentWarning, PackageState, PackageDevState ContentWarning, PackageState, PackageDevState
from .utils import isYes, get_int_or_abort from .utils import isYes, get_int_or_abort
@ -17,11 +17,11 @@ class QueryBuilder:
title = "Packages" title = "Packages"
# Get request types # Get request types
types = args.getlist("type") # types = args.getlist("type")
types = [PackageType.get(tname) for tname in types] # types = [PackageType.get(tname) for tname in types]
types = [type for type in types if type is not None] # types = [type for type in types if type is not None]
if len(types) > 0: # if len(types) > 0:
title = ", ".join([str(type.plural) for type in types]) # title = ", ".join([str(type.plural) for type in types])
# Get tags types # Get tags types
tags = args.getlist("tag") tags = args.getlist("tag")
@ -32,7 +32,7 @@ class QueryBuilder:
self.hide_flags = set(args.getlist("hide")) self.hide_flags = set(args.getlist("hide"))
self.title = title self.title = title
self.types = types # self.types = types
self.tags = tags self.tags = tags
self.random = "random" in args self.random = "random" in args
@ -126,8 +126,8 @@ class QueryBuilder:
return query return query
def filterPackageQuery(self, query): def filterPackageQuery(self, query):
if len(self.types) > 0: # if len(self.types) > 0:
query = query.filter(Package.type.in_(self.types)) # query = query.filter(Package.type.in_(self.types))
if self.author: if self.author:
author = User.query.filter_by(username=self.author).first() author = User.query.filter_by(username=self.author).first()
@ -233,8 +233,8 @@ class QueryBuilder:
query = query.filter(or_(ForumTopic.title.ilike('%' + self.search + '%'), query = query.filter(or_(ForumTopic.title.ilike('%' + self.search + '%'),
ForumTopic.name == self.search.lower())) ForumTopic.name == self.search.lower()))
if len(self.types) > 0: # if len(self.types) > 0:
query = query.filter(ForumTopic.type.in_(self.types)) # query = query.filter(ForumTopic.type.in_(self.types))
if self.limit: if self.limit:
query = query.limit(self.limit) query = query.limit(self.limit)

View File

@ -21,6 +21,16 @@ from app.utils import make_flask_login_password
from app.utils.image import get_image_size from app.utils.image import get_image_size
from app.utils import randomString 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 :| #Workaround to get the urls because app.get_urls() doesn't work :|
def get_urls(app): def get_urls(app):
kinds = [AppStreamGlib.UrlKind(kind) for kind in range(11)] kinds = [AppStreamGlib.UrlKind(kind) for kind in range(11)]
@ -80,23 +90,39 @@ def importFromFlathub():
game1.state = PackageState.APPROVED game1.state = PackageState.APPROVED
game1.name = app.get_id() game1.name = app.get_id()
game1.title = app.get_name() game1.title = app.get_name()
if "Development" in app.get_categories(): hashtags = []
game1.type = PackageType.TOOL
else:
game1.type = PackageType.GAME
license = "Uknown" if app.get_project_license() is None else app.get_project_license().split("AND")[0].split("and")[0] 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: if license not in licenses:
row = License(license) row = License(license)
licenses[row.name] = row licenses[row.name] = row
session.add(row) session.add(row)
session.commit() session.commit()
for category in app.get_categories(): has_toplevel = False
if category.lower() not in tags: categories = list(set([ x.lower() for x in app.get_categories() ]))
row = Tag(category.lower()) added = []
tags[row.name] = row # how do we get the type attribute from <component> here?
print("adding tag: ", row.name) # if app.get_type() == "addon":
session.add(row) # game1.tags.append(tags["mods"])
game1.tags.append(tags[category.lower()]) # 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 # this short list seems like a reasonable set of initial "featured" games
if app.get_id() in alwaysAccept: if app.get_id() in alwaysAccept:
@ -110,12 +136,16 @@ def importFromFlathub():
for url,t in urls: for url,t in urls:
if t == "bugtracker": if t == "bugtracker":
game1.issueTracker = url 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 game1.repo = url
if t == "homepage" and "git" not in url:
game1.website = url
game1.forums = 12835 game1.forums = 12835
game1.short_desc = "" or app.get_comment() 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 = "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 += f"\n```\nflatpak install flathub {app.get_id()}\n```\n"
game1.install_desc += "Run: \n" game1.install_desc += "Run: \n"

View File

@ -117,76 +117,76 @@ def getLinksFromModSearch():
return links return links
@celery.task() # @celery.task()
def importTopicList(): # def importTopicList():
links_by_id = getLinksFromModSearch() # links_by_id = getLinksFromModSearch()
info_by_id = {} # info_by_id = {}
getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': False }) # 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(9, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': True })
getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False }) # getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False })
getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True }) # getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True })
# Caches # # Caches
username_to_user = {} # username_to_user = {}
topics_by_id = {} # topics_by_id = {}
for topic in ForumTopic.query.all(): # for topic in ForumTopic.query.all():
topics_by_id[topic.topic_id] = topic # topics_by_id[topic.topic_id] = topic
def get_or_create_user(username): # def get_or_create_user(username):
user = username_to_user.get(username) # user = username_to_user.get(username)
if user: # if user:
return user # return user
if not is_username_valid(username): # if not is_username_valid(username):
return None # return None
user = User.query.filter_by(forums_username=username).first() # user = User.query.filter_by(forums_username=username).first()
if user is None: # if user is None:
user = User.query.filter_by(username=username).first() # user = User.query.filter_by(username=username).first()
if user: # if user:
return None # return None
user = User(username) # user = User(username)
user.forums_username = username # user.forums_username = username
db.session.add(user) # db.session.add(user)
username_to_user[username] = user # username_to_user[username] = user
return user # return user
# Create or update # # Create or update
for info in info_by_id.values(): # for info in info_by_id.values():
id = int(info["id"]) # id = int(info["id"])
# Get author # # Get author
username = info["author"] # username = info["author"]
user = get_or_create_user(username) # user = get_or_create_user(username)
if user is None: # if user is None:
print("Error! Unable to create user {}".format(username), file=sys.stderr) # print("Error! Unable to create user {}".format(username), file=sys.stderr)
continue # continue
# Get / add row # # Get / add row
topic = topics_by_id.get(id) # topic = topics_by_id.get(id)
if topic is None: # if topic is None:
topic = ForumTopic() # topic = ForumTopic()
db.session.add(topic) # db.session.add(topic)
# Parse title # # Parse title
title, name = parseTitle(info["title"]) # title, name = parseTitle(info["title"])
# Get link # # Get link
link = links_by_id.get(id) # link = links_by_id.get(id)
# Fill row # # Fill row
topic.topic_id = int(id) # topic.topic_id = int(id)
topic.author = user # topic.author = user
topic.type = info["type"] # topic.type = info["type"]
topic.title = title # topic.title = title
topic.name = name # topic.name = name
topic.link = link # topic.link = link
topic.wip = info["wip"] # topic.wip = info["wip"]
topic.posts = int(info["posts"]) # topic.posts = int(info["posts"])
topic.views = int(info["views"]) # topic.views = int(info["views"])
topic.created_at = info["date"] # topic.created_at = info["date"]
db.session.commit() # db.session.commit()

View File

@ -75,8 +75,7 @@ def getMeta(urlstr, author):
def postReleaseCheckUpdate(self, release: PackageRelease, path): def postReleaseCheckUpdate(self, release: PackageRelease, path):
try: try:
tree = build_tree(path, expected_type=ContentType[release.package.type.name], tree = build_tree(path, author=release.package.author.username, name=release.package.name)
author=release.package.author.username, name=release.package.name)
cache = {} cache = {}
@ -109,9 +108,9 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
db.session.add(Dependency(package, meta=meta, optional=True)) db.session.add(Dependency(package, meta=meta, optional=True))
# Update game supports # Update game supports
if package.type == PackageType.TOOL: # if package.type == PackageType.TOOL:
resolver = GameSupportResolver() # resolver = GameSupportResolver()
resolver.update(package) # resolver.update(package)
# # Update min/max # # Update min/max
# if tree.meta.get("min_minetest_version"): # if tree.meta.get("min_minetest_version"):

View File

@ -6,32 +6,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title> <title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=34"> <link rel="stylesheet" type="text/css" href="/static/custom.css?v=35">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" /> <link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16"> <link rel="icon" href="/static/lg-logo.png" sizes="290x290">
<link rel="icon" href="/favicon-128.png" sizes="128x128">
<link rel="icon" href="/favicon-32.png" sizes="32x32">
{% block headextra %}{% endblock %} {% block headextra %}{% endblock %}
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container"> <div class="container">
<a class="navbar-brand" href="/">{{ config.USER_APP_NAME }}</a> <a class="navbar-brand" href="/">
<img src="/static/lg-logo.png" width="30" height="30" class="d-inline-block align-top" alt="">
{{ config.USER_APP_NAME }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarColor01"> <div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
{% for tag in toplevel %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('packages.list_all', tag='game') }}">{{ _("Games") }}</a> <a class="nav-link" href="{{ url_for('packages.list_all', tag=tag.name) }}">{{ _(tag.title) }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('packages.list_all', tag='development') }}">{{ _("Tools") }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('packages.list_all', tag='gtk') }}">{{ _("Asset Packs") }}</a>
</li> </li>
{% endfor %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('packages.list_all', random=1, lucky=1) }}">{{ _("Random") }}</a> <a class="nav-link" href="{{ url_for('packages.list_all', random=1, lucky=1) }}">{{ _("Random") }}</a>
</li> </li>
@ -45,7 +42,7 @@
<form class="form-inline my-2 my-lg-0" method="GET" action="/packages/"> <form class="form-inline my-2 my-lg-0" method="GET" action="/packages/">
{% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %} {% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %}
<input class="form-control" name="q" type="text" <input class="form-control" name="q" type="text"
placeholder="{% if query_hint %}{{ _('Search %(type)s', type=query_hint | lower) }}{% else %}{{ _('Search all packages') }}{% endif %}" placeholder="{% if query_hint %}{{ _('Search projects', type=query_hint | lower) }}{% else %}{{ _('Search all packages') }}{% endif %}"
value="{{ query or ''}}"> value="{{ query or ''}}">
<input class="btn btn-secondary my-2 my-sm-0 mr-sm-2" type="submit" value="{{ _('Search') }}" /> <input class="btn btn-secondary my-2 my-sm-0 mr-sm-2" type="submit" value="{{ _('Search') }}" />
<!-- <input class="btn btn-secondary my-2 my-sm-0" <!-- <input class="btn btn-secondary my-2 my-sm-0"

View File

@ -34,5 +34,5 @@
{{ _("Unsubscribe") }} {{ _("Unsubscribe") }}
</a> <br> </a> <br>
{{ _("This is a '%(type)s' notification.", type=notification.type.getTitle()) }} {{ _("This is a 'projects' notification.", type=notification.type.getTitle()) }}
{% endblock %} {% endblock %}

View File

@ -38,17 +38,11 @@
alt="{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}"> alt="{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}">
</div> </div>
<div class="carousel-caption text-shadow"> <div class="carousel-caption text-shadow">
<h3 class="mt-0 mb-3">
<strong>{{ package.title }}</strong>
</h3>
<p> <p>
{{ package.short_desc }} {{ package.short_desc }}
</p> </p>
{% if package.author %} {% if package.author %}
<div class="d-none d-md-block"> <div class="d-none d-md-block">
<span class="mr-2">
{{ package.type.text }}
</span>
{% for warning in package.content_warnings %} {% for warning in package.content_warnings %}
<span class="badge badge-warning" title="{{ warning.description }}"> <span class="badge badge-warning" title="{{ warning.description }}">
<i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i> <i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i>
@ -106,27 +100,13 @@
<h2 class="my-3">{{ _("Recently Updated") }}</h2> <h2 class="my-3">{{ _("Recently Updated") }}</h2>
{{ render_pkggrid(updated) }} {{ render_pkggrid(updated) }}
{% for tag in toplevel %}
<a href="{{ url_for('packages.list_all', type='game', sort='score', order='desc') }}" class="btn btn-secondary float-right"> <a href="{{ url_for('packages.list_all', tag=tag.name, sort='score', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }} {{ _("See more") }}
</a> </a>
<h2 class="my-3">{{ _("Top Games") }}</h2> <h2 class="my-3">{{ _("Top " + tag.title) }}</h2>
{{ render_pkggrid(pop_gam) }} {{ render_pkggrid(popular[tag.name]) }}
{% endfor %}
<a href="{{ url_for('packages.list_all', type='tool', sort='score', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Tools") }}</h2>
{{ render_pkggrid(pop_mod) }}
<a href="{{ url_for('packages.list_all', type='asset_pack', sort='score', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Asset Packs") }}</h2>
{{ render_pkggrid(pop_txp) }}
<h2 class="my-3">{{ _("Search by Tags") }}</h2> <h2 class="my-3">{{ _("Search by Tags") }}</h2>
{% for pair in tags %} {% for pair in tags %}

View File

@ -37,7 +37,7 @@
{% endif %} {% endif %}
{% endset %} {% endset %}
{% elif (package.type == package.type.GAME or package.type == package.type.ASSETPACK) and package.screenshots.count() == 0 %} {% elif package.screenshots.count() == 0 %}
{% set message = _("You need to add at least one screenshot.") %} {% set message = _("You need to add at least one screenshot.") %}
{% elif package.getMissingHardDependenciesQuery().count() > 0 %} {% elif package.getMissingHardDependenciesQuery().count() > 0 %}

View File

@ -20,11 +20,11 @@
{{ package.short_desc }} {{ package.short_desc }}
</p> </p>
{% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.ASSETPACK %} {% if not package.license.is_foss and not package.media_license.is_foss %}
<p style="color:#f33;"> <p style="color:#f33;">
{{ _("<b>Warning:</b> Non-free code and media.") }} {{ _("<b>Warning:</b> Non-free code and media.") }}
</p> </p>
{% elif not package.license.is_foss and package.type != package.type.ASSETPACK %} {% elif not package.license.is_foss %}
<p style="color:#f33;"> <p style="color:#f33;">
{{ _("<b>Warning:</b> Non-free code.") }} {{ _("<b>Warning:</b> Non-free code.") }}
</p> </p>

View File

@ -106,7 +106,7 @@
<form method="post" action="{{ package.getURL("packages.review") }}" class="card-body"> <form method="post" action="{{ package.getURL("packages.review") }}" class="card-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<p> <p>
{{ _("Do you recommend this %(type)s?", type=package.type.text | lower) }} {{ _("Do you recommend this?") }}
</p> </p>
<div class="btn-group btn-group-toggle" data-toggle="buttons"> <div class="btn-group btn-group-toggle" data-toggle="buttons">
@ -145,7 +145,7 @@
<form method="post" action="{{ package.getURL("packages.review") }}" class="card-body"> <form method="post" action="{{ package.getURL("packages.review") }}" class="card-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<p> <p>
{{ _("Do you recommend this %(type)s?", type=package.type.text | lower) }} {{ _("Do you recommend this?") }}
</p> </p>
<div class="btn-group"> <div class="btn-group">

View File

@ -46,7 +46,7 @@
<div class="alert alert-secondary"> <div class="alert alert-secondary">
<a class="float-right btn btn-sm btn-default" href="/help/package_config/#cdbjson">{{ _("Read more") }}</a> <a class="float-right btn btn-sm btn-default" href="/help/package_config/#cdbjson">{{ _("Read more") }}</a>
{{ _("You can include a .cdb.json file in your %(type)s to update these details automatically.", type=package.type.text.lower()) }} {{ _("You can include a .cdb.json file in your projects to update these details automatically.") }}
</div> </div>
{% endif %} {% endif %}

View File

@ -40,13 +40,13 @@
<h2 class="my-3">{{ _("Recently Updated") }}</h2> <h2 class="my-3">{{ _("Recently Updated") }}</h2>
{{ render_pkggrid(updated) }} {{ render_pkggrid(updated) }}
{% for tag in toplevel %}
<a href="{{ url_for('packages.list_all', type='tool', sort='score', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right"> <a href="{{ url_for('packages.list_all', tag=tag.name, sort='score', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
{{ _("See more") }} {{ _("See more") }}
</a> </a>
<h2 class="my-3">{{ _("Top Tools") }}</h2> <h2 class="my-3">{{ _("Top " + tag.title) }}</h2>
{{ render_pkggrid(pop_mod) }} {{ render_pkggrid(popular[tag.name]) }}
{% endfor %}
<a href="{{ url_for('packages.list_all', sort='reviews', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right"> <a href="{{ url_for('packages.list_all', sort='reviews', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
{{ _("See more") }} {{ _("See more") }}

View File

@ -7,7 +7,7 @@
{% block author_links %} {% block author_links %}
{% if authors %} {% if authors %}
{% for author in authors %} {% for author in authors %}
<a href="{{ url_for('packages.list_all', type=type, author=author[0], q=author[1]) }}">{{ author[0] }}</a> <a href="{{ url_for('packages.list_all', author=author[0], q=author[1]) }}">{{ author[0] }}</a>
{% if not loop.last %} {% if not loop.last %}
, ,
{% endif %} {% endif %}

View File

@ -11,8 +11,7 @@
<h1>{{ self.title() }}</h1> <h1>{{ self.title() }}</h1>
<p> <p>
{{ _("A release is a single downloadable version of your %(title)s.", title=package.type.text.lower()) }} {{ _("A release is a single downloadable version of your project") }}
{{ _("You need to create releases even if you use a rolling release development cycle, as Minetest needs them to check for updates.") }}
</p> </p>
{% if package.repo %} {% if package.repo %}

View File

@ -32,7 +32,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<p> <p>
{{ _("Do you recommend this %(type)s?", type=package.type.text | lower) }} {{ _("Do you recommend this?") }}
</p> </p>
{{ render_toggle_field(form.recommends, icons={"yes":"fa-thumbs-up", "no":"fa-thumbs-down"}) }} {{ render_toggle_field(form.recommends, icons={"yes":"fa-thumbs-up", "no":"fa-thumbs-down"}) }}

View File

@ -47,9 +47,9 @@
{% endblock %} {% endblock %}
{% block container %} {% block container %}
{% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.ASSETPACK %} {% if not package.license.is_foss and not package.media_license.is_foss %}
{% set package_warning=_("Non-free code and media") %} {% set package_warning=_("Non-free code and media") %}
{% elif not package.license.is_foss and package.type != package.type.ASSETPACK %} {% elif not package.license.is_foss %}
{% set package_warning=_("Non-free code") %} {% set package_warning=_("Non-free code") %}
{% elif not package.media_license.is_foss %} {% elif not package.media_license.is_foss %}
{% set package_warning=_("Non-free media") %} {% set package_warning=_("Non-free media") %}
@ -230,13 +230,13 @@
{% for ss in screenshots %} {% for ss in screenshots %}
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %} {% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
<li> <li>
<a href="{{ ss.url }}" class="gallery-image"> <a href="{{ss.url}}" class="gallery-image" data-toggle="modal" data-target="#screenshot_{{ss.id}}">
<img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" /> <img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
{% if not ss.approved %} {% if not ss.approved %}
<span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span> <span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span>
{% endif %} {% endif %}
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% else %} {% else %}
<li> <li>
@ -307,12 +307,10 @@
{{ render_pkggrid(packages_uses) }} {{ render_pkggrid(packages_uses) }}
{% endif %} {% endif %}
{% if package.type == package.type.GAME %} <h2>{{ _("Content") }}</h2>
<h2>{{ _("Content") }}</h2> <a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary">
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary"> {{ _("View content for game") }}
{{ _("View content for game") }} </a>
</a>
{% endif %}
</div> </div>
<aside class="col-md-3 info-sidebar"> <aside class="col-md-3 info-sidebar">
@ -362,92 +360,81 @@
</div> </div>
{% endif %} {% endif %}
{% if package.type == package.type.GAME %} <a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary">
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary"> {{ _("View content for game") }}
{{ _("View content for game") }} </a>
</a> <h3>{{ _("Dependencies") }}</h3>
{% endif %} <dl>
<dt>{{ _("Required") }}</dt>
<dd>
{% for dep in package.getSortedHardDependencies() %}
{%- if dep.package %}
<a class="badge badge-primary"
href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }}
</a>
{% elif dep.meta_package %}
<a class="badge badge-primary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
</a>
{% else %}
{{ "Expected package or meta_package in dep!" | throw }}
{% endif %}
{% else %}
{{ _("No required dependencies") }}
{% endfor %}
</dd>
{% if package.type != package.type.ASSETPACK %} {% set optional_deps=package.getSortedOptionalDependencies() %}
<h3>{{ _("Dependencies") }}</h3> {% if optional_deps %}
<dl> <dt>{{ _("Optional") }}</dt>
<dt>{{ _("Required") }}</dt>
<dd> <dd>
{% for dep in package.getSortedHardDependencies() %} {% for dep in optional_deps %}
{%- if dep.package %} {%- if dep.package %}
<a class="badge badge-primary" <a class="badge badge-secondary"
href="{{ dep.package.getURL("packages.view") }}"> href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s", {{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }} title=dep.package.title, display_name=dep.package.author.display_name) }}
</a>
{% elif dep.meta_package %} {% elif dep.meta_package %}
<a class="badge badge-primary" <a class="badge badge-secondary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}"> href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }} {{ dep.meta_package.name }}
</a>
{% else %} {% else %}
{{ "Expected package or meta_package in dep!" | throw }} {{ "Expected package or meta_package in dep!" | throw }}
{% endif %} {% endif %}</a>
{% else %}
{{ _("No required dependencies") }}
{% endfor %} {% endfor %}
</dd> </dd>
{% endif %}
</dl>
{% set optional_deps=package.getSortedOptionalDependencies() %} <h3>{{ _("Compatible Games") }}</h3>
{% if optional_deps %} {% for support in package.getSortedSupportedGames() %}
<dt>{{ _("Optional") }}</dt> <a class="badge badge-secondary"
<dd> href="{{ support.game.getURL('packages.view') }}">
{% for dep in optional_deps %} {{ _("%(title)s by %(display_name)s",
{%- if dep.package %} title=support.game.title, display_name=support.game.author.display_name) }}
<a class="badge badge-secondary" </a>
href="{{ dep.package.getURL("packages.view") }}"> {% else %}
{{ _("%(title)s by %(display_name)s", {{ _("No specific game is required") }}
title=dep.package.title, display_name=dep.package.author.display_name) }} {% endfor %}
{% elif dep.meta_package %} <p class="text-muted small mt-2 mb-0">
<a class="badge badge-secondary" {{ _("This is an experimental feature.") }}
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}"> {{ _("Supported games are determined by an algorithm, and may not be correct.") }}
{{ dep.meta_package.name }} </p>
{% else %}
{{ "Expected package or meta_package in dep!" | throw }}
{% endif %}</a>
{% endfor %}
</dd>
{% endif %}
</dl>
{% endif %}
{% if package.type == package.type.TOOL %}
<h3>{{ _("Compatible Games") }}</h3>
{% for support in package.getSortedSupportedGames() %}
<a class="badge badge-secondary"
href="{{ support.game.getURL('packages.view') }}">
{{ _("%(title)s by %(display_name)s",
title=support.game.title, display_name=support.game.author.display_name) }}
</a>
{% else %}
{{ _("No specific game is required") }}
{% endfor %}
<p class="text-muted small mt-2 mb-0">
{{ _("This is an experimental feature.") }}
{{ _("Supported games are determined by an algorithm, and may not be correct.") }}
</p>
{% endif %}
<h3> <h3>
{{ _("Information") }} {{ _("Information") }}
</h3> </h3>
<dl> <dl>
<dt>{{ _("Type") }}</dt>
<dd>{{ package.type.text }}</dd>
<dt>{{ _("Technical Name") }}</dt> <dt>{{ _("Technical Name") }}</dt>
<dd>{{ package.name }}</dd> <dd>{{ package.name }}</dd>
<dt>{{ _("License") }}</dt> <dt>{{ _("License") }}</dt>
<dd> <dd>
{% if package.license == package.media_license %} {% if package.license == package.media_license %}
{{ render_license(package.license) }} {{ render_license(package.license) }}
{% elif package.type == package.type.ASSETPACK %}
{{ render_license(package.media_license) }}
{% else %} {% else %}
{{ _("%(code_license)s for code,<br>%(media_license)s for media.", {{ _("%(code_license)s for code,<br>%(media_license)s for media.",
code_license=render_license(package.license), media_license=render_license(package.media_license)) }} code_license=render_license(package.license), media_license=render_license(package.media_license)) }}
@ -536,5 +523,25 @@
</aside> </aside>
</div> </div>
</section> </section>
{% for ss in screenshots %}
<div class="modal fade" id="screenshot_{{ss.id}}" tabindex="-1" aria-labelledby="screenshot" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="screenshot title">"{{ ss.title }}"</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<img src="{{ ss.url }}" alt="{{ ss.title }}" style="width: 100%"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endfor %}
</main> </main>
{% endblock %} {% endblock %}

View File

@ -29,7 +29,7 @@
<i class="fab fa-github mr-1"></i> <i class="fab fa-github mr-1"></i>
{{ _("GitHub") }} {{ _("GitHub") }}
</a> </a>
<a class="btn btn-secondary" href="{{ url_for('users.claim') }}"> <a class="btn btn-secondary" href="{{ url_for('users.register') }}">
<i class="fas fa-user-plus mr-1"></i> <i class="fas fa-user-plus mr-1"></i>
{{ _("Register") }} {{ _("Register") }}
</a> </a>

View File

@ -33,7 +33,7 @@
</p> </p>
<p> <p>
<img src="/static/puzzle.png" /> <img src="{{captcha}}" />
</p> </p>
{{ render_field(form.question, hint=_("Please prove that you are human")) }} {{ render_field(form.question, hint=_("Please prove that you are human")) }}

View File

@ -1,7 +1,7 @@
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
from app.default_data import populate_test_data from app.default_data import populate_test_data
from app.models import db, License, PackageType, User, Package, PackageState, PackageRelease, MinetestRelease from app.models import db, License, User, Package, PackageState, PackageRelease, MinetestRelease
from .utils import parse_json, validate_package_list from .utils import parse_json, validate_package_list
from .utils import client # noqa from .utils import client # noqa
@ -16,30 +16,30 @@ def make_package(name: str, versions: List[Tuple[Optional[str], Optional[str]]])
tool.title = name tool.title = name
tool.license = license tool.license = license
tool.media_license = license tool.media_license = license
tool.type = PackageType.TOOL # tool.type = PackageType.TOOL
tool.author = author tool.author = author
tool.short_desc = "The content library should not be used yet as it is still in alpha" tool.short_desc = "The content library should not be used yet as it is still in alpha"
tool.desc = "This is the long desc" tool.desc = "This is the long desc"
db.session.add(tool) db.session.add(tool)
rels = [] # rels = []
for (minv, maxv) in versions: # for (minv, maxv) in versions:
rel = PackageRelease() # rel = PackageRelease()
rel.package = tool # rel.package = tool
rel.title = "test" # rel.title = "test"
rel.url = "https://github.com/ezhh/handholds/archive/master.zip" # rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
# if minv: # # if minv:
# rel.min_rel = MinetestRelease.query.filter_by(name=minv).first() # # rel.min_rel = MinetestRelease.query.filter_by(name=minv).first()
# assert rel.min_rel # # assert rel.min_rel
# if maxv: # # if maxv:
# rel.max_rel = MinetestRelease.query.filter_by(name=maxv).first() # # rel.max_rel = MinetestRelease.query.filter_by(name=maxv).first()
# assert rel.max_rel # # assert rel.max_rel
rel.approved = True # rel.approved = True
db.session.add(rel) # db.session.add(rel)
rels.append(rel) # rels.append(rel)
db.session.flush() db.session.flush()

View File

@ -23,6 +23,9 @@ from .user import *
YESES = ["yes", "true", "1", "on"] YESES = ["yes", "true", "1", "on"]
def get_toplevel_tags():
return Tag.query.filter_by(is_toplevel=True).order_by(db.asc(Tag.id)).all()
def is_username_valid(username): def is_username_valid(username):
return username is not None and len(username) >= 2 and re.match(r"^[A-Za-z0-9._-]*$", username) return username is not None and len(username) >= 2 and re.match(r"^[A-Za-z0-9._-]*$", username)

View File

@ -46,8 +46,8 @@ alwaysAccept = [
'org.freecol.FreeCol', 'org.freecol.FreeCol',
'org.freeciv.Freeciv', 'org.freeciv.Freeciv',
'io.github.EndlessSky.endless-sky', 'io.github.EndlessSky.endless-sky',
'org.frozen_bubble.frozen-bubble',
'org.kde.ksudoku', 'org.kde.ksudoku',
'net.veloren.veloren'
] ]
alwaysDeny = [ alwaysDeny = [

View File

@ -18,7 +18,7 @@
from functools import wraps from functools import wraps
from flask import abort, redirect, url_for, request from flask import abort, redirect, url_for, request
from flask_login import current_user from flask_login import current_user
from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, ThreadReply, Thread, PackageState, PackageType, PackageAlias from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, ThreadReply, Thread, PackageState, PackageAlias
def getPackageByInfo(author, name): def getPackageByInfo(author, name):
@ -45,7 +45,7 @@ def is_package_page(f):
package = getPackageByInfo(author, name) package = getPackageByInfo(author, name)
if package is None: if package is None:
package = getPackageByInfo(author, name + "_game") package = getPackageByInfo(author, name + "_game")
if package and package.type == PackageType.GAME: if package:
args = dict(kwargs) args = dict(kwargs)
args["name"] = name + "_game" args["name"] = name + "_game"
return redirect(url_for(request.endpoint, **args)) return redirect(url_for(request.endpoint, **args))

View File

@ -18,6 +18,37 @@ depends_on = None
def upgrade(): def upgrade():
command = """
CREATE OR REPLACE FUNCTION parse_websearch(config regconfig, search_query text)
RETURNS tsquery AS $$
SELECT
string_agg(
(
CASE
WHEN position('''' IN words.word) > 0 THEN CONCAT(words.word, ':*')
ELSE words.word
END
),
' '
)::tsquery
FROM (
SELECT trim(
regexp_split_to_table(
websearch_to_tsquery(config, lower(search_query))::text,
' '
)
) AS word
) AS words
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION parse_websearch(search_query text)
RETURNS tsquery AS $$
SELECT parse_websearch('pg_catalog.simple', search_query);
$$ LANGUAGE SQL IMMUTABLE;"""
op.execute(command)
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('content_warning', op.create_table('content_warning',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
@ -96,7 +127,6 @@ def upgrade():
sa.Column('desc', sa.UnicodeText(), nullable=True), sa.Column('desc', sa.UnicodeText(), nullable=True),
sa.Column('build_desc', sa.UnicodeText(), nullable=True), sa.Column('build_desc', sa.UnicodeText(), nullable=True),
sa.Column('install_desc', sa.UnicodeText(), nullable=True), sa.Column('install_desc', sa.UnicodeText(), nullable=True),
sa.Column('type', sa.Enum('GAME', 'TOOL', 'ASSETPACK', name='packagetype'), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('approved_at', sa.DateTime(), nullable=True), sa.Column('approved_at', sa.DateTime(), nullable=True),
sa.Column('search_vector', sqlalchemy_utils.types.ts_vector.TSVectorType(), nullable=True), sa.Column('search_vector', sqlalchemy_utils.types.ts_vector.TSVectorType(), nullable=True),
@ -113,9 +143,7 @@ def upgrade():
sa.Column('issueTracker', sa.String(length=200), nullable=True), sa.Column('issueTracker', sa.String(length=200), nullable=True),
sa.Column('forums', sa.Integer(), nullable=True), sa.Column('forums', sa.Integer(), nullable=True),
sa.Column('video_url', sa.String(length=200), nullable=True), sa.Column('video_url', sa.String(length=200), nullable=True),
# sa.Column('cover_image_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ), sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
# sa.ForeignKeyConstraint(['cover_image_id'], ['package_screenshot.id'], ),
sa.ForeignKeyConstraint(['license_id'], ['license.id'], ), sa.ForeignKeyConstraint(['license_id'], ['license.id'], ),
sa.ForeignKeyConstraint(['media_license_id'], ['license.id'], ), sa.ForeignKeyConstraint(['media_license_id'], ['license.id'], ),
sa.ForeignKeyConstraint(['review_thread_id'], ['thread.id'], ), sa.ForeignKeyConstraint(['review_thread_id'], ['thread.id'], ),

View File

@ -7,6 +7,7 @@ beautifulsoup4==4.10.0
billiard==3.6.4.0 billiard==3.6.4.0
bleach==4.1.0 bleach==4.1.0
blinker==1.4 blinker==1.4
git+https://github.com/lepture/captcha.git@2792068
celery==5.2.3 celery==5.2.3
certifi==2021.10.8 certifi==2021.10.8
cffi==1.15.0 cffi==1.15.0

View File

@ -18,6 +18,7 @@ passlib
pygments pygments
beautifulsoup4 beautifulsoup4
captcha
celery celery
kombu kombu
GitPython GitPython

View File

@ -776,18 +776,18 @@ msgstr "Sie befinden sich auf dem %(place)s. Platz."
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "Top %(type)s" msgstr "Top projects"
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "Top %(group)d %(type)s" msgstr "Top %(group)d projects"
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s hat ein %(type)s auf dem %(place)d. Platz." msgstr "%(display_name)s hat ein projects auf dem %(place)d. Platz."
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
#, python-format #, python-format
@ -1054,8 +1054,8 @@ msgstr "Themen"
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "Suche %(type)s" msgstr "Suche projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13 #: app/templates/todo/tags.html:13
@ -1386,8 +1386,8 @@ msgstr "Verwalten Sie Ihre Einstellungen"
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "Dies ist eine „%(type)s“-Benachrichtigung." msgstr "Dies ist eine „projects“-Benachrichtigung."
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
#: app/templates/emails/notification_digest.html:29 #: app/templates/emails/notification_digest.html:29
@ -1665,8 +1665,8 @@ msgstr "Rezension"
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "Empfehlen Sie dieses %(type)s?" msgstr "Empfehlen Sie dieses projects?"
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40 #: app/templates/packages/review_create_edit.html:40
@ -1898,10 +1898,10 @@ msgstr "Weiterlesen"
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""
"Sie können eine .cdb.json-Datei in Ihre %(type)s einfügen, um diese " "Sie können eine .cdb.json-Datei in Ihre projects einfügen, um diese "
"Details automatisch zu aktualisieren." "Details automatisch zu aktualisieren."
#: app/templates/packages/create_edit.html:55 #: app/templates/packages/create_edit.html:55

View File

@ -774,18 +774,18 @@ msgstr "Estás en el lugar %(place)s."
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s tiene un %(type)s en el puesto #%(place)d." msgstr "%(display_name)s tiene un projects en el puesto #%(place)d."
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
#, python-format #, python-format
@ -1044,8 +1044,8 @@ msgstr "Hilos de discusión"
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "Buscar %(type)s" msgstr "Buscar projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13 #: app/templates/todo/tags.html:13
@ -1376,8 +1376,8 @@ msgstr "Administrar tus preferencias"
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "Esta es una notificación de %(type)s." msgstr "Esta es una notificación de projects."
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
#: app/templates/emails/notification_digest.html:29 #: app/templates/emails/notification_digest.html:29
@ -1639,8 +1639,8 @@ msgstr "Reseñar"
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "¿Recomienda este %(type)s?" msgstr "¿Recomienda este projects?"
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40 #: app/templates/packages/review_create_edit.html:40
@ -1870,10 +1870,10 @@ msgstr "Leer más"
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""
"Puede incluir un archivo .cdb.json en su %(type)s para actualizar estos " "Puede incluir un archivo .cdb.json en su projects para actualizar estos "
"detalles automáticamente." "detalles automáticamente."
#: app/templates/packages/create_edit.html:55 #: app/templates/packages/create_edit.html:55

View File

@ -776,18 +776,18 @@ msgstr "Vous êtes à la %(place)s. place."
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "Top %(type)s" msgstr "Top projects"
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "Top %(group)d %(type)s" msgstr "Top %(group)d projects"
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s a un %(type)s à la #%(place)d place." msgstr "%(display_name)s a un projects à la #%(place)d place."
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
#, python-format #, python-format
@ -1049,8 +1049,8 @@ msgstr "Fils"
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "Rechercher %(type)s" msgstr "Rechercher projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13 #: app/templates/todo/tags.html:13
@ -1383,8 +1383,8 @@ msgstr "Gérez vos préférences"
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "Il s'agit d'une notification « %(type)s »." msgstr "Il s'agit d'une notification « projects »."
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
#: app/templates/emails/notification_digest.html:29 #: app/templates/emails/notification_digest.html:29
@ -1650,8 +1650,8 @@ msgstr "Évaluation"
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "Recommandez-vous ce %(type)s ?" msgstr "Recommandez-vous ce projects ?"
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40 #: app/templates/packages/review_create_edit.html:40
@ -1884,10 +1884,10 @@ msgstr "Lire plus"
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""
"Vous pouvez inclure un fichier .cdb.json dans votre %(type)s pour mettre " "Vous pouvez inclure un fichier .cdb.json dans votre projects pour mettre "
"à jour ces détails automatiquement." "à jour ces détails automatiquement."
#: app/templates/packages/create_edit.html:55 #: app/templates/packages/create_edit.html:55

View File

@ -786,17 +786,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
@ -1053,7 +1053,7 @@ msgstr ""
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "" msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1377,7 +1377,7 @@ msgstr ""
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "" msgstr ""
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
@ -1624,7 +1624,7 @@ msgstr ""
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "" msgstr ""
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
@ -1851,7 +1851,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""

View File

@ -768,18 +768,18 @@ msgstr "Anda berada pada urutan %(place)s."
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "%(type)s teratas" msgstr "projects teratas"
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "%(group)d %(type)s teratas" msgstr "%(group)d projects teratas"
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s memiliki sebuah %(type)s yang ada di urutan ke-%(place)d." msgstr "%(display_name)s memiliki sebuah projects yang ada di urutan ke-%(place)d."
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
#, python-format #, python-format
@ -1043,8 +1043,8 @@ msgstr "Utas"
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "Cari %(type)s" msgstr "Cari projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13 #: app/templates/todo/tags.html:13
@ -1373,8 +1373,8 @@ msgstr "Kelola pilihan Anda"
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "Ini adalah pemberitahuan '%(type)s'." msgstr "Ini adalah pemberitahuan 'projects'."
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
#: app/templates/emails/notification_digest.html:29 #: app/templates/emails/notification_digest.html:29
@ -1636,8 +1636,8 @@ msgstr "Ulasan"
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "Apa Anda menyarankan %(type)s ini?" msgstr "Apa Anda menyarankan projects ini?"
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40 #: app/templates/packages/review_create_edit.html:40
@ -1865,10 +1865,10 @@ msgstr "Baca lebih banyak"
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""
"Anda dapat menyisipkan berkas .cdb.json dalam %(type)s Anda untuk " "Anda dapat menyisipkan berkas .cdb.json dalam projects Anda untuk "
"memperbarui detail ini secara otomatis." "memperbarui detail ini secara otomatis."
#: app/templates/packages/create_edit.html:55 #: app/templates/packages/create_edit.html:55

View File

@ -748,17 +748,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
@ -1008,7 +1008,7 @@ msgstr ""
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "" msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1332,7 +1332,7 @@ msgstr ""
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "" msgstr ""
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
@ -1579,7 +1579,7 @@ msgstr ""
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "" msgstr ""
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
@ -1806,7 +1806,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""

View File

@ -748,17 +748,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
@ -1008,7 +1008,7 @@ msgstr ""
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "" msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1332,7 +1332,7 @@ msgstr ""
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "" msgstr ""
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
@ -1579,7 +1579,7 @@ msgstr ""
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "" msgstr ""
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
@ -1806,7 +1806,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""

View File

@ -774,18 +774,18 @@ msgstr "Anda berada di kedudukan %(place)s."
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "%(type)s teratas" msgstr "projects teratas"
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "%(type)s %(group)d teratas" msgstr "projects %(group)d teratas"
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s mempunyai suatu %(type)s berkedudukan #%(place)d." msgstr "%(display_name)s mempunyai suatu projects berkedudukan #%(place)d."
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
#, python-format #, python-format
@ -1049,8 +1049,8 @@ msgstr "Bebenang"
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "Cari %(type)s" msgstr "Cari projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13 #: app/templates/todo/tags.html:13
@ -1381,8 +1381,8 @@ msgstr "Uruskan keutamaan anda"
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "Ini pemberitahuan '%(type)s'." msgstr "Ini pemberitahuan 'projects'."
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
#: app/templates/emails/notification_digest.html:29 #: app/templates/emails/notification_digest.html:29
@ -1648,8 +1648,8 @@ msgstr "Ulasan"
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "Adakah anda mengesyorkan %(type)s ini?" msgstr "Adakah anda mengesyorkan projects ini?"
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40 #: app/templates/packages/review_create_edit.html:40
@ -1879,10 +1879,10 @@ msgstr "Baca lanjut"
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""
"Anda boleh sertakan fail .cdb.json dalam %(type)s anda untuk kemas kini " "Anda boleh sertakan fail .cdb.json dalam projects anda untuk kemas kini "
"maklumat ini secara automatiknya." "maklumat ini secara automatiknya."
#: app/templates/packages/create_edit.html:55 #: app/templates/packages/create_edit.html:55

View File

@ -750,17 +750,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
@ -1011,7 +1011,7 @@ msgstr ""
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "" msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1335,7 +1335,7 @@ msgstr ""
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "" msgstr ""
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
@ -1582,7 +1582,7 @@ msgstr ""
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "" msgstr ""
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
@ -1809,7 +1809,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""

View File

@ -770,18 +770,18 @@ msgstr "Вы на %(place)s месте."
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "Топ %(type)s" msgstr "Топ projects"
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "Топ %(group)d %(type)s" msgstr "Топ %(group)d projects"
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s имеет %(type)s на #%(place)d." msgstr "%(display_name)s имеет projects на #%(place)d."
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
#, python-format #, python-format
@ -1045,8 +1045,8 @@ msgstr "Треды"
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "Искать %(type)s" msgstr "Искать projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13 #: app/templates/todo/tags.html:13
@ -1382,8 +1382,8 @@ msgstr "Управляйте своими предпочтениями"
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "Это '%(type)s' уведомление." msgstr "Это 'projects' уведомление."
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
#: app/templates/emails/notification_digest.html:29 #: app/templates/emails/notification_digest.html:29
@ -1655,8 +1655,8 @@ msgstr "Обзор"
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "Рекомендуете ли вы этот %(type)s?" msgstr "Рекомендуете ли вы этот projects?"
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40 #: app/templates/packages/review_create_edit.html:40
@ -1888,10 +1888,10 @@ msgstr "Читать далее"
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""
"Вы можете включить файл .cdb.json в свой %(type)s для автоматического " "Вы можете включить файл .cdb.json в свой projects для автоматического "
"обновления этих данных." "обновления этих данных."
#: app/templates/packages/create_edit.html:55 #: app/templates/packages/create_edit.html:55

View File

@ -750,17 +750,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
@ -1010,7 +1010,7 @@ msgstr ""
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "" msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1334,7 +1334,7 @@ msgstr ""
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "" msgstr ""
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
@ -1581,7 +1581,7 @@ msgstr ""
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "" msgstr ""
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
@ -1808,7 +1808,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""

View File

@ -754,17 +754,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
@ -1014,7 +1014,7 @@ msgstr ""
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "" msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1338,7 +1338,7 @@ msgstr ""
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "" msgstr ""
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
@ -1585,7 +1585,7 @@ msgstr ""
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "" msgstr ""
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
@ -1812,7 +1812,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""

View File

@ -752,18 +752,18 @@ msgstr "你在第%(place)s位。"
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "最高%(type)s" msgstr "最高projects"
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "最高 %(group)d %(type)s" msgstr "最高 %(group)d projects"
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s 有一个 %(type)s 放置在 #%(place)d 处。" msgstr "%(display_name)s 有一个 projects 放置在 #%(place)d 处。"
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
#, python-format #, python-format
@ -1020,7 +1020,7 @@ msgstr ""
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "" msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1358,7 +1358,7 @@ msgstr ""
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "" msgstr ""
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
@ -1610,7 +1610,7 @@ msgstr ""
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "" msgstr ""
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
@ -1840,7 +1840,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""

View File

@ -753,17 +753,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161 #: app/blueprints/users/profile.py:161
#, python-format #, python-format
msgid "Top %(type)s" msgid "Top projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:163 #: app/blueprints/users/profile.py:163
#, python-format #, python-format
msgid "Top %(group)d %(type)s" msgid "Top %(group)d projects"
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:172 #: app/blueprints/users/profile.py:172
#, python-format #, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d." msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "" msgstr ""
#: app/blueprints/users/profile.py:187 #: app/blueprints/users/profile.py:187
@ -1014,7 +1014,7 @@ msgstr ""
#: app/templates/base.html:48 #: app/templates/base.html:48
#, python-format #, python-format
msgid "Search %(type)s" msgid "Search projects"
msgstr "" msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11 #: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1338,7 +1338,7 @@ msgstr ""
#: app/templates/emails/notification.html:37 #: app/templates/emails/notification.html:37
#, python-format #, python-format
msgid "This is a '%(type)s' notification." msgid "This is a 'projects' notification."
msgstr "" msgstr ""
#: app/templates/emails/notification_digest.html:14 #: app/templates/emails/notification_digest.html:14
@ -1585,7 +1585,7 @@ msgstr ""
#: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148 #: app/templates/macros/reviews.html:109 app/templates/macros/reviews.html:148
#: app/templates/packages/review_create_edit.html:35 #: app/templates/packages/review_create_edit.html:35
#, python-format #, python-format
msgid "Do you recommend this %(type)s?" msgid "Do you recommend this projects?"
msgstr "" msgstr ""
#: app/templates/macros/reviews.html:124 #: app/templates/macros/reviews.html:124
@ -1812,7 +1812,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49 #: app/templates/packages/create_edit.html:49
#, python-format #, python-format
msgid "" msgid ""
"You can include a .cdb.json file in your %(type)s to update these details" "You can include a .cdb.json file in your projects to update these details"
" automatically." " automatically."
msgstr "" msgstr ""