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

View File

@ -25,10 +25,10 @@ from sqlalchemy.sql.expression import func
from app import csrf
from app.markdown import render_markdown
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, ForumTopic, \
from app.models import Tag, PackageState, Package, db, PackageRelease, Permission, ForumTopic, \
APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread
from app.querybuilder import QueryBuilder
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes, get_toplevel_tags
from . import bp
from .auth import is_api_authd
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
@ -89,9 +89,6 @@ def resolve_package_deps(out, package, only_hard, depth=1):
ret = []
out[id] = ret
if package.type != PackageType.TOOL:
return
for dep in package.dependencies:
if only_hard and dep.optional:
continue
@ -106,7 +103,7 @@ def resolve_package_deps(out, package, only_hard, depth=1):
fulfilled_by = [ pkg.getId() for pkg in dep.meta_package.packages]
if depth == 1 and not dep.optional:
most_likely = next((pkg for pkg in dep.meta_package.packages if pkg.type == PackageType.TOOL), None)
most_likely = next((pkg for pkg in dep.meta_package.packages), None)
if most_likely:
resolve_package_deps(out, most_likely, only_hard, depth + 1)
@ -470,9 +467,11 @@ def homepage():
featured = query.filter(Package.tags.any(name="featured")).order_by(
func.random()).limit(6).all()
new = query.order_by(db.desc(Package.approved_at)).limit(4).all()
pop_mod = query.filter_by(type=PackageType.TOOL).order_by(db.desc(Package.score)).limit(8).all()
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(8).all()
pop_txp = query.filter_by(type=PackageType.ASSETPACK).order_by(db.desc(Package.score)).limit(8).all()
toplevel_tags = get_toplevel_tags()
popular = {}
# new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
for toplevel_tag in toplevel_tags:
popular[toplevel_tag.name] = query.filter(Package.tags.any(name=toplevel_tag.name)).order_by(db.desc(Package.score)).limit(8).all()
high_reviewed = query.order_by(db.desc(Package.score - Package.score_downloads)) \
.filter(Package.reviews.any()).limit(4).all()
@ -487,16 +486,15 @@ def homepage():
def mapPackages(packages: List[Package]):
return [pkg.getAsDictionaryShort(current_app.config["BASE_URL"]) for pkg in packages]
popular = { k:mapPackages(v) for (k, v) in popular.items() }
return jsonify({
"count": count,
"downloads": downloads,
"featured": mapPackages(featured),
"new": mapPackages(new),
"updated": mapPackages(updated),
"pop_mod": mapPackages(pop_mod),
"pop_txp": mapPackages(pop_txp),
"pop_game": mapPackages(pop_gam),
"popular": popular,
"toplevel": [ x.getAsDictionary() for x in toplevel_tags ],
"high_reviewed": mapPackages(high_reviewed)
})
@ -505,7 +503,7 @@ def homepage():
@cors_allowed
def welcome_v1():
featured = Package.query \
.filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED,
.filter(Package.state == PackageState.APPROVED,
Package.tags.any(name="featured")) \
.order_by(func.random()) \
.limit(5).all()
@ -520,23 +518,6 @@ def welcome_v1():
"featured": map_packages(featured),
})
# @bp.route("/api/minetest_versions/")
# @cors_allowed
# def versions():
# protocol_version = request.args.get("protocol_version")
# engine_version = request.args.get("engine_version")
# if protocol_version or engine_version:
# rel = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version))
# if rel is None:
# error(404, "No releases found")
# return jsonify(rel.getAsDictionary())
# return jsonify([rel.getAsDictionary() \
# for rel in MinetestRelease.query.all() if rel.getActual() is not None])
@bp.route("/api/dependencies/")
@cors_allowed
def all_deps():
@ -545,7 +526,7 @@ def all_deps():
def format_pkg(pkg: Package):
return {
"type": pkg.type.toName(),
# "type": pkg.type.toName(),
"author": pkg.author.username,
"name": pkg.name,
"provides": [x.name for x in pkg.provides],

View File

@ -3,6 +3,7 @@ from flask import Blueprint, render_template, redirect
bp = Blueprint("homepage", __name__)
from app.models import *
from app.utils import get_toplevel_tags
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import func
@ -18,11 +19,12 @@ def home():
count = query.count()
featured = query.filter(Package.tags.any(name="featured")).order_by(func.random()).limit(6).all()
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
popular = {}
new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
pop_mod = join(query.filter_by(type=PackageType.TOOL).order_by(db.desc(Package.score))).limit(8).all()
pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all()
pop_txp = join(query.filter_by(type=PackageType.ASSETPACK).order_by(db.desc(Package.score))).limit(8).all()
for toplevel_tag in toplevel_tags:
popular[toplevel_tag.name] = join(query.filter(Package.tags.any(name=toplevel_tag.name)).order_by(db.desc(Package.score))).limit(8).all()
high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \
.filter(Package.reviews.any()).limit(4).all()
@ -41,4 +43,4 @@ def home():
.select_from(Tag).outerjoin(Tags).group_by(Tag.id).order_by(db.asc(Tag.title)).all()
return render_template("index.html", count=count, downloads=downloads, tags=tags, featured=featured,
new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam, high_reviewed=high_reviewed, reviews=reviews)
new=new, updated=updated, popular=popular, toplevel=toplevel_tags, high_reviewed=high_reviewed, reviews=reviews)

View File

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

View File

@ -99,10 +99,12 @@ def list_all():
selected_tags = set(qb.tags)
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
return render_template("packages/list.html",
query_hint=title, packages=query.items, pagination=query,
query=search, tags=tags, selected_tags=selected_tags, type=type_name,
authors=authors, packages_count=query.total, topics=topics)
authors=authors, packages_count=query.total, topics=topics, toplevel=toplevel_tags)
def getReleases(package):
@ -120,7 +122,7 @@ def view(package):
package.checkPerm(current_user, Permission.APPROVE_NEW))
conflicting_modnames = None
if show_similar and package.type != PackageType.ASSETPACK:
if show_similar:
conflicting_modnames = db.session.query(MetaPackage.name) \
.filter(MetaPackage.id.in_([ mp.id for mp in package.provides ])) \
.filter(MetaPackage.packages.any(Package.id != package.id)) \
@ -136,14 +138,12 @@ def view(package):
conflicting_modnames = set([x[0] for x in conflicting_modnames])
packages_uses = None
if package.type == PackageType.TOOL:
packages_uses = Package.query.filter(
Package.type == PackageType.TOOL,
Package.id != package.id,
Package.state == PackageState.APPROVED,
Package.dependencies.any(
Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
.order_by(db.desc(Package.score)).limit(6).all()
packages_uses = Package.query.filter(
Package.id != package.id,
Package.state == PackageState.APPROVED,
Package.dependencies.any(
Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
.order_by(db.desc(Package.score)).limit(6).all()
releases = getReleases(package)
@ -178,11 +178,13 @@ def view(package):
has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
return render_template("packages/view.html",
package=package, releases=releases, packages_uses=packages_uses,
conflicting_modnames=conflicting_modnames,
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl,
threads=threads.all(), has_review=has_review)
threads=threads.all(), has_review=has_review, toplevel=toplevel_tags)
@bp.route("/packages/<author>/<name>/shields/<type>/")
@ -226,7 +228,14 @@ def makeLabel(obj):
class PackageForm(FlaskForm):
type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.GAME)
try:
toplevel_tags = [ x.title for x in Tag.query.filter_by(is_toplevel=True).all() ]
type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=toplevel_tags, default=toplevel_tags[0])
except:
toplevel_tags = []
# type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=toplevel_tags, default=toplevel_tags[0])
# type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.GAME)
title = StringField(lazy_gettext("Title (Human-readable)"), [InputRequired(), Length(1, 100)])
name = StringField(lazy_gettext("Name (Technical)"), [InputRequired(), Length(1, 100), Regexp("^[a-zA-Z0-9_\-\.]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_), dashes and periods only"))])
short_desc = StringField(lazy_gettext("Short Description (Plaintext)"), [InputRequired(), Length(1,200)])
@ -293,9 +302,6 @@ def create_edit(author=None, name=None):
form.tags.data = package.tags
form.content_warnings.data = package.content_warnings
if request.method == "POST" and form.type.data == PackageType.ASSETPACK:
form.license.data = form.media_license.data
if form.validate_on_submit():
wasNew = False
if not package:
@ -353,7 +359,7 @@ def create_edit(author=None, name=None):
form=form, author=author, enable_wizard=enableWizard,
packages=package_query.all(),
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all(),
tabs=get_package_tabs(current_user, package), current_tab="edit")
tabs=get_package_tabs(current_user, package), current_tab="edit", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/state/", methods=["POST"])
@ -408,7 +414,7 @@ def move_to_state(package):
def remove(package):
if request.method == "GET":
return render_template("packages/remove.html", package=package,
tabs=get_package_tabs(current_user, package), current_tab="remove")
tabs=get_package_tabs(current_user, package), current_tab="remove", toplevel=get_toplevel_tags())
reason = request.form.get("reason") or "?"
@ -501,7 +507,7 @@ def edit_maintainers(package):
users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).order_by(db.asc(User.username)).all()
return render_template("packages/edit_maintainers.html", package=package, form=form,
users=users, tabs=get_package_tabs(current_user, package), current_tab="maintainers")
users=users, tabs=get_package_tabs(current_user, package), current_tab="maintainers", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/remove-self-maintainer/", methods=["POST"])
@ -540,7 +546,7 @@ def audit(package):
pagination = query.paginate(page, num, True)
return render_template("packages/audit.html", log=pagination.items, pagination=pagination,
package=package, tabs=get_package_tabs(current_user, package), current_tab="audit")
package=package, tabs=get_package_tabs(current_user, package), current_tab="audit", toplevel=get_toplevel_tags())
class PackageAliasForm(FlaskForm):
@ -554,7 +560,7 @@ class PackageAliasForm(FlaskForm):
@rank_required(UserRank.EDITOR)
@is_package_page
def alias_list(package: Package):
return render_template("packages/alias_list.html", package=package)
return render_template("packages/alias_list.html", package=package, toplevel=get_toplevel_tags())
@bp.route("/packages/<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 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/")
@ -588,7 +594,7 @@ def alias_create_edit(package: Package, alias_id: int = None):
@is_package_page
def share(package):
return render_template("packages/share.html", package=package,
tabs=get_package_tabs(current_user, package), current_tab="share")
tabs=get_package_tabs(current_user, package), current_tab="share", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/similar/")
@ -610,4 +616,4 @@ def similar(package):
# .all()
return render_template("packages/similar.html", package=package,
packages_modnames=packages_modnames, similar_topics=[])
packages_modnames=packages_modnames, similar_topics=[], toplevel=get_toplevel_tags())

View File

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

View File

@ -26,7 +26,7 @@ from wtforms import *
from wtforms.validators import *
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \
Permission, AuditSeverity
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog, get_toplevel_tags
from app.tasks.webhooktasks import post_discord_webhook
@ -36,7 +36,7 @@ def list_reviews():
num = min(40, get_int_or_abort(request.args.get("n"), 100))
pagination = PackageReview.query.order_by(db.desc(PackageReview.created_at)).paginate(page, num, True)
return render_template("packages/reviews_list.html", pagination=pagination, reviews=pagination.items)
return render_template("packages/reviews_list.html", pagination=pagination, reviews=pagination.items, toplevel=get_toplevel_tags())
class ReviewForm(FlaskForm):
@ -123,7 +123,7 @@ def review(package):
return redirect(package.getURL("packages.view"))
return render_template("packages/review_create_edit.html",
form=form, package=package, review=review)
form=form, package=package, review=review, toplevel=get_toplevel_tags())
@bp.route("/packages/<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))
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()
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"])
@ -92,7 +92,7 @@ def create_screenshot(package):
except LogicError as e:
flash(e.message, "danger")
return render_template("packages/screenshot_new.html", package=package, form=form)
return render_template("packages/screenshot_new.html", package=package, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
@ -124,7 +124,7 @@ def edit_screenshot(package, id):
db.session.commit()
return redirect(package.getURL("packages.screenshots"))
return render_template("packages/screenshot_edit.html", package=package, screenshot=screenshot, form=form)
return render_template("packages/screenshot_edit.html", package=package, screenshot=screenshot, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<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/>.
import base64
import string
import random
from flask import *
from flask_babel import gettext, lazy_gettext, get_locale
from flask_login import current_user, login_required, logout_user, login_user
@ -23,6 +25,7 @@ from flask_wtf import FlaskForm
from sqlalchemy import or_
from wtforms import *
from wtforms.validators import *
from captcha.image import ImageCaptcha
from app.models import *
from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email
@ -105,13 +108,13 @@ class RegisterForm(FlaskForm):
Regexp("^[a-zA-Z0-9._-]+$", message=lazy_gettext("Only a-zA-Z0-9._ allowed"))])
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(6, 100)])
question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()])
question = StringField(lazy_gettext("Type the characters from the image above."), [InputRequired()])
agree = BooleanField(lazy_gettext("I agree"), [DataRequired()])
submit = SubmitField(lazy_gettext("Register"))
def handle_register(form):
if form.question.data.strip().lower() != "19":
if form.question.data.strip().lower() != session["captcha-solution"]:
flash(gettext("Incorrect captcha answer"), "danger")
return
@ -181,7 +184,13 @@ def register():
if ret:
return ret
return render_template("users/register.html", form=form,
image = ImageCaptcha()
characters = string.ascii_letters + string.digits
solution = ''.join(random.choice(characters) for i in range(4))
data = image.generate(solution)
session["captcha-solution"] = solution
b64data = "data:image/png;base64," + base64.b64encode(data.read()).decode("utf-8")
return render_template("users/register.html", form=form, captcha=b64data,
suggested_password=genphrase(entropy=52, wordset="bip39"))

View File

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

View File

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

View File

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

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.
<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

View File

@ -20,7 +20,7 @@ import sys
from typing import List, Dict, Optional, Iterator, Iterable
from app.logic.LogicError import LogicError
from app.models import Package, MetaPackage, PackageType, PackageState, PackageGameSupport, db
from app.models import Package, MetaPackage, PackageState, PackageGameSupport, db
"""
get_game_support(package):
@ -128,39 +128,36 @@ class GameSupportResolver:
history = history.copy()
history.append(key)
if package.type == PackageType.GAME:
return PackageSet([package])
# if package.type == PackageType.GAME:
return PackageSet([package])
if key in self.resolved_packages:
return self.resolved_packages.get(key)
# if key in self.resolved_packages:
# return self.resolved_packages.get(key)
if key in self.checked_packages:
print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
return PackageSet()
# if key in self.checked_packages:
# print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
# return PackageSet()
self.checked_packages.add(key)
# self.checked_packages.add(key)
if package.type != PackageType.TOOL:
raise LogicError(500, "Got non-tool")
# retval = PackageSet()
retval = PackageSet()
# for dep in package.dependencies.filter_by(optional=False).all():
# ret = self.resolve_for_meta_package(dep.meta_package, history)
# if len(ret) == 0:
# continue
# elif len(retval) == 0:
# retval.update(ret)
# else:
# retval.intersection_update(ret)
# if len(retval) == 0:
# raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games")
for dep in package.dependencies.filter_by(optional=False).all():
ret = self.resolve_for_meta_package(dep.meta_package, history)
if len(ret) == 0:
continue
elif len(retval) == 0:
retval.update(ret)
else:
retval.intersection_update(ret)
if len(retval) == 0:
raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games")
self.resolved_packages[key] = retval
return retval
# self.resolved_packages[key] = retval
# return retval
def update_all(self) -> None:
for package in Package.query.filter(Package.type == PackageType.TOOL, Package.state != PackageState.DELETED).all():
for package in Package.query.filter(Package.state != PackageState.DELETED).all():
retval = self.resolve(package, [])
for game in retval:
support = PackageGameSupport(package, game)

View File

@ -20,7 +20,7 @@ import validators
from flask_babel import lazy_gettext
from app.logic.LogicError import LogicError
from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \
from app.models import User, Package, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \
License, UserRank, PackageDevState
from app.utils import addAuditLog
from app.utils.url import clean_youtube_url
@ -118,8 +118,8 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
validate(data)
if "type" in data:
data["type"] = PackageType.coerce(data["type"])
# if "type" in data:
# data["type"] = PackageType.coerce(data["type"])
if "dev_state" in data:
data["dev_state"] = PackageDevState.coerce(data["dev_state"])
@ -140,12 +140,12 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
if key in data:
setattr(package, key, data[key])
if package.type == PackageType.ASSETPACK:
package.license = package.media_license
# if package.type == PackageType.ASSETPACK:
# package.license = package.media_license
if was_new and package.type == PackageType.TOOL:
m = MetaPackage.GetOrCreate(package.name, {})
package.provides.append(m)
# if was_new and package.type == PackageType.TOOL:
# m = MetaPackage.GetOrCreate(package.name, {})
# package.provides.append(m)
if "tags" in data:
old_tags = list(package.tags)

View File

@ -120,7 +120,7 @@ class ForumTopic(db.Model):
wip = db.Column(db.Boolean, default=False, nullable=False)
discarded = db.Column(db.Boolean, default=False, nullable=False)
type = db.Column(db.Enum(PackageType), nullable=False)
# type = db.Column(db.Enum(PackageType), nullable=False)
title = db.Column(db.String(200), nullable=False)
name = db.Column(db.String(30), nullable=True)
link = db.Column(db.String(200), nullable=True)

View File

@ -48,49 +48,49 @@ class License(db.Model):
return self.name
class PackageType(enum.Enum):
GAME = "Game"
TOOL = "Tool"
ASSETPACK = "Asset Pack"
# class PackageType(enum.Enum):
# GAME = "Game"
# TOOL = "Tool"
# ASSETPACK = "Asset Pack"
def toName(self):
return self.name.lower()
# def toName(self):
# return self.name.lower()
def __str__(self):
return self.name
# def __str__(self):
# return self.name
@property
def text(self):
if self == PackageType.TOOL:
return lazy_gettext("Tool")
elif self == PackageType.GAME:
return lazy_gettext("Game")
elif self == PackageType.ASSETPACK:
return lazy_gettext("Asset Pack")
# @property
# def text(self):
# if self == PackageType.TOOL:
# return lazy_gettext("Tool")
# elif self == PackageType.GAME:
# return lazy_gettext("Game")
# elif self == PackageType.ASSETPACK:
# return lazy_gettext("Asset Pack")
@property
def plural(self):
if self == PackageType.TOOL:
return lazy_gettext("Tools")
elif self == PackageType.GAME:
return lazy_gettext("Games")
elif self == PackageType.ASSETPACK:
return lazy_gettext("Asset Packs")
# @property
# def plural(self):
# if self == PackageType.TOOL:
# return lazy_gettext("Tools")
# elif self == PackageType.GAME:
# return lazy_gettext("Games")
# elif self == PackageType.ASSETPACK:
# return lazy_gettext("Asset Packs")
@classmethod
def get(cls, name):
try:
return PackageType[name.upper()]
except KeyError:
return None
# @classmethod
# def get(cls, name):
# try:
# return PackageType[name.upper()]
# except KeyError:
# return None
@classmethod
def choices(cls):
return [(choice, choice.text) for choice in cls]
# @classmethod
# def choices(cls):
# return [(choice, choice.text) for choice in cls]
@classmethod
def coerce(cls, item):
return item if type(item) == PackageType else PackageType[item.upper()]
# @classmethod
# def coerce(cls, item):
# return item if type(item) == PackageType else PackageType[item.upper()]
class PackageDevState(enum.Enum):
@ -218,7 +218,7 @@ class PackagePropertyKey(enum.Enum):
title = "Title"
short_desc = "Short Description"
desc = "Description"
type = "Type"
# type = "Type"
license = "License"
media_license = "Media License"
tags = "Tags"
@ -378,7 +378,7 @@ class Package(db.Model):
desc = db.Column(db.UnicodeText, nullable=True)
build_desc = db.Column(db.UnicodeText, nullable=True)
install_desc = db.Column(db.UnicodeText, nullable=True)
type = db.Column(db.Enum(PackageType), nullable=False)
# type = db.Column(db.Enum(PackageType), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
approved_at = db.Column(db.DateTime, nullable=True, default=None)
@ -534,7 +534,7 @@ class Package(db.Model):
"title": self.title,
"author": self.author.username,
"short_description": short_desc,
"type": self.type.toName(),
# "type": self.type.toName(),
"release": release_id,
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
"aliases": [ alias.getAsDictionary() for alias in self.aliases ],
@ -559,7 +559,7 @@ class Package(db.Model):
"title": self.title,
"short_description": self.short_desc,
"long_description": self.desc,
"type": self.type.toName(),
# "type": self.type.toName(),
"created_at": self.created_at.isoformat(),
"license": self.license.name,
@ -771,7 +771,7 @@ class MetaPackage(db.Model):
dependencies = db.relationship("Dependency", back_populates="meta_package", lazy="dynamic")
packages = db.relationship("Package", lazy="dynamic", back_populates="provides", secondary=PackageProvides)
mp_name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'")
mp_name_valid = db.CheckConstraint("name ~* '^[a-zA-Z0-9_\-\.]+$'")
def __init__(self, name=None):
self.name = name
@ -872,48 +872,6 @@ class Tag(db.Model):
}
# class MinetestRelease(db.Model):
# id = db.Column(db.Integer, primary_key=True)
# name = db.Column(db.String(100), unique=True, nullable=False)
# protocol = db.Column(db.Integer, nullable=False, default=0)
# def __init__(self, name=None, protocol=0):
# self.name = name
# self.protocol = protocol
# def getActual(self):
# return None if self.name == "None" else self
# def getAsDictionary(self):
# return {
# "name": self.name,
# "protocol_version": self.protocol,
# "is_dev": "-dev" in self.name,
# }
# @classmethod
# def get(cls, version, protocol_num):
# if version:
# parts = version.strip().split(".")
# if len(parts) >= 2:
# major_minor = parts[0] + "." + parts[1]
# query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor)))
# if protocol_num:
# query = query.filter_by(protocol=protocol_num)
# release = query.one_or_none()
# if release:
# return release
# if protocol_num:
# # Find the closest matching release
# return MinetestRelease.query.order_by(db.desc(MinetestRelease.protocol),
# db.desc(MinetestRelease.id)) \
# .filter(MinetestRelease.protocol <= protocol_num).first()
# return None
class PackageRelease(db.Model):
id = db.Column(db.Integer, primary_key=True)
@ -929,11 +887,6 @@ class PackageRelease(db.Model):
downloads = db.Column(db.Integer, nullable=False, default=0)
channel = db.Column(db.String(200), nullable=False, default="")
# min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
# min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
# max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
# max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
# If the release is approved, then the task_id must be null and the url must be present
CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
@ -951,8 +904,6 @@ class PackageRelease(db.Model):
"commit": self.commit_hash,
"downloads": self.downloads,
"channel": self.channel,
# "min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(),
# "max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(),
}
def getLongAsDictionary(self):
@ -963,8 +914,6 @@ class PackageRelease(db.Model):
"release_date": self.releaseDate.isoformat(),
"commit": self.commit_hash,
"downloads": self.downloads,
# "min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(),
# "max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(),
"channel": self.channel,
"package": self.package.getAsDictionaryKey()
}

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.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
from .utils import isYes, get_int_or_abort
@ -17,11 +17,11 @@ class QueryBuilder:
title = "Packages"
# Get request types
types = args.getlist("type")
types = [PackageType.get(tname) for tname in types]
types = [type for type in types if type is not None]
if len(types) > 0:
title = ", ".join([str(type.plural) for type in types])
# types = args.getlist("type")
# types = [PackageType.get(tname) for tname in types]
# types = [type for type in types if type is not None]
# if len(types) > 0:
# title = ", ".join([str(type.plural) for type in types])
# Get tags types
tags = args.getlist("tag")
@ -32,7 +32,7 @@ class QueryBuilder:
self.hide_flags = set(args.getlist("hide"))
self.title = title
self.types = types
# self.types = types
self.tags = tags
self.random = "random" in args
@ -126,8 +126,8 @@ class QueryBuilder:
return query
def filterPackageQuery(self, query):
if len(self.types) > 0:
query = query.filter(Package.type.in_(self.types))
# if len(self.types) > 0:
# query = query.filter(Package.type.in_(self.types))
if self.author:
author = User.query.filter_by(username=self.author).first()
@ -233,8 +233,8 @@ class QueryBuilder:
query = query.filter(or_(ForumTopic.title.ilike('%' + self.search + '%'),
ForumTopic.name == self.search.lower()))
if len(self.types) > 0:
query = query.filter(ForumTopic.type.in_(self.types))
# if len(self.types) > 0:
# query = query.filter(ForumTopic.type.in_(self.types))
if self.limit:
query = query.limit(self.limit)

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

View File

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

View File

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

View File

@ -6,32 +6,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<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/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="shortcut icon" href="/favicon-16.png" sizes="16x16">
<link rel="icon" href="/favicon-128.png" sizes="128x128">
<link rel="icon" href="/favicon-32.png" sizes="32x32">
<link rel="icon" href="/static/lg-logo.png" sizes="290x290">
{% block headextra %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<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">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav mr-auto">
{% for tag in toplevel %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('packages.list_all', tag='game') }}">{{ _("Games") }}</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>
<a class="nav-link" href="{{ url_for('packages.list_all', tag=tag.name) }}">{{ _(tag.title) }}</a>
</li>
{% endfor %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('packages.list_all', random=1, lucky=1) }}">{{ _("Random") }}</a>
</li>
@ -45,7 +42,7 @@
<form class="form-inline my-2 my-lg-0" method="GET" action="/packages/">
{% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %}
<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 ''}}">
<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"

View File

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

View File

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

View File

@ -37,7 +37,7 @@
{% endif %}
{% 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.") %}
{% elif package.getMissingHardDependenciesQuery().count() > 0 %}

View File

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

View File

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

View File

@ -46,7 +46,7 @@
<div class="alert alert-secondary">
<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>
{% endif %}

View File

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

View File

@ -7,7 +7,7 @@
{% block author_links %}
{% if 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 %}
,
{% endif %}

View File

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

View File

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

View File

@ -47,9 +47,9 @@
{% endblock %}
{% 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") %}
{% elif not package.license.is_foss and package.type != package.type.ASSETPACK %}
{% elif not package.license.is_foss %}
{% set package_warning=_("Non-free code") %}
{% elif not package.media_license.is_foss %}
{% set package_warning=_("Non-free media") %}
@ -230,13 +230,13 @@
{% for ss in screenshots %}
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
<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 }}" />
{% if not ss.approved %}
<span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span>
{% endif %}
</a>
</li>
</li>
{% endif %}
{% else %}
<li>
@ -307,12 +307,10 @@
{{ render_pkggrid(packages_uses) }}
{% endif %}
{% if package.type == package.type.GAME %}
<h2>{{ _("Content") }}</h2>
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary">
{{ _("View content for game") }}
</a>
{% endif %}
<h2>{{ _("Content") }}</h2>
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary">
{{ _("View content for game") }}
</a>
</div>
<aside class="col-md-3 info-sidebar">
@ -362,92 +360,81 @@
</div>
{% endif %}
{% if package.type == package.type.GAME %}
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary">
{{ _("View content for game") }}
</a>
{% endif %}
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary">
{{ _("View content for game") }}
</a>
<h3>{{ _("Dependencies") }}</h3>
<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 %}
<h3>{{ _("Dependencies") }}</h3>
<dl>
<dt>{{ _("Required") }}</dt>
{% set optional_deps=package.getSortedOptionalDependencies() %}
{% if optional_deps %}
<dt>{{ _("Optional") }}</dt>
<dd>
{% for dep in package.getSortedHardDependencies() %}
{% for dep in optional_deps %}
{%- if dep.package %}
<a class="badge badge-primary"
<a class="badge badge-secondary"
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"
<a class="badge badge-secondary"
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") }}
{% endif %}</a>
{% endfor %}
</dd>
{% endif %}
</dl>
{% set optional_deps=package.getSortedOptionalDependencies() %}
{% if optional_deps %}
<dt>{{ _("Optional") }}</dt>
<dd>
{% for dep in optional_deps %}
{%- if dep.package %}
<a class="badge badge-secondary"
href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }}
{% elif dep.meta_package %}
<a class="badge badge-secondary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
{% 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>{{ _("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>
<h3>
{{ _("Information") }}
</h3>
<dl>
<dt>{{ _("Type") }}</dt>
<dd>{{ package.type.text }}</dd>
<dt>{{ _("Technical Name") }}</dt>
<dd>{{ package.name }}</dd>
<dt>{{ _("License") }}</dt>
<dd>
{% if package.license == package.media_license %}
{{ render_license(package.license) }}
{% elif package.type == package.type.ASSETPACK %}
{{ render_license(package.media_license) }}
{% else %}
{{ _("%(code_license)s for code,<br>%(media_license)s for media.",
code_license=render_license(package.license), media_license=render_license(package.media_license)) }}
@ -536,5 +523,25 @@
</aside>
</div>
</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>
{% endblock %}

View File

@ -29,7 +29,7 @@
<i class="fab fa-github mr-1"></i>
{{ _("GitHub") }}
</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>
{{ _("Register") }}
</a>

View File

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

View File

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

View File

@ -23,6 +23,9 @@ from .user import *
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):
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.freeciv.Freeciv',
'io.github.EndlessSky.endless-sky',
'org.frozen_bubble.frozen-bubble',
'org.kde.ksudoku',
'net.veloren.veloren'
]
alwaysDeny = [

View File

@ -18,7 +18,7 @@
from functools import wraps
from flask import abort, redirect, url_for, request
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):
@ -45,7 +45,7 @@ def is_package_page(f):
package = getPackageByInfo(author, name)
if package is None:
package = getPackageByInfo(author, name + "_game")
if package and package.type == PackageType.GAME:
if package:
args = dict(kwargs)
args["name"] = name + "_game"
return redirect(url_for(request.endpoint, **args))

View File

@ -18,6 +18,37 @@ depends_on = None
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! ###
op.create_table('content_warning',
sa.Column('id', sa.Integer(), nullable=False),
@ -96,7 +127,6 @@ def upgrade():
sa.Column('desc', sa.UnicodeText(), nullable=True),
sa.Column('build_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('approved_at', sa.DateTime(), 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('forums', sa.Integer(), 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(['cover_image_id'], ['package_screenshot.id'], ),
sa.ForeignKeyConstraint(['license_id'], ['license.id'], ),
sa.ForeignKeyConstraint(['media_license_id'], ['license.id'], ),
sa.ForeignKeyConstraint(['review_thread_id'], ['thread.id'], ),

View File

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

View File

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

View File

@ -776,18 +776,18 @@ msgstr "Sie befinden sich auf dem %(place)s. Platz."
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgstr "Top %(type)s"
msgid "Top projects"
msgstr "Top projects"
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgstr "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr "Top %(group)d projects"
#: app/blueprints/users/profile.py:172
#, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d."
msgstr "%(display_name)s hat ein %(type)s auf dem %(place)d. Platz."
msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s hat ein projects auf dem %(place)d. Platz."
#: app/blueprints/users/profile.py:187
#, python-format
@ -1054,8 +1054,8 @@ msgstr "Themen"
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgstr "Suche %(type)s"
msgid "Search projects"
msgstr "Suche projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13
@ -1386,8 +1386,8 @@ msgstr "Verwalten Sie Ihre Einstellungen"
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgstr "Dies ist eine „%(type)s“-Benachrichtigung."
msgid "This is a 'projects' notification."
msgstr "Dies ist eine „projects“-Benachrichtigung."
#: app/templates/emails/notification_digest.html:14
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgstr "Empfehlen Sie dieses %(type)s?"
msgid "Do you recommend this projects?"
msgstr "Empfehlen Sie dieses projects?"
#: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40
@ -1898,10 +1898,10 @@ msgstr "Weiterlesen"
#: app/templates/packages/create_edit.html:49
#, python-format
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."
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."
#: 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
#, python-format
msgid "Top %(type)s"
msgid "Top projects"
msgstr ""
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr ""
#: app/blueprints/users/profile.py:172
#, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d."
msgstr "%(display_name)s tiene un %(type)s en el puesto #%(place)d."
msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s tiene un projects en el puesto #%(place)d."
#: app/blueprints/users/profile.py:187
#, python-format
@ -1044,8 +1044,8 @@ msgstr "Hilos de discusión"
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgstr "Buscar %(type)s"
msgid "Search projects"
msgstr "Buscar projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13
@ -1376,8 +1376,8 @@ msgstr "Administrar tus preferencias"
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgstr "Esta es una notificación de %(type)s."
msgid "This is a 'projects' notification."
msgstr "Esta es una notificación de projects."
#: app/templates/emails/notification_digest.html:14
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgstr "¿Recomienda este %(type)s?"
msgid "Do you recommend this projects?"
msgstr "¿Recomienda este projects?"
#: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40
@ -1870,10 +1870,10 @@ msgstr "Leer más"
#: app/templates/packages/create_edit.html:49
#, python-format
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."
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."
#: 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
#, python-format
msgid "Top %(type)s"
msgstr "Top %(type)s"
msgid "Top projects"
msgstr "Top projects"
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgstr "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr "Top %(group)d projects"
#: app/blueprints/users/profile.py:172
#, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d."
msgstr "%(display_name)s a un %(type)s à la #%(place)d place."
msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s a un projects à la #%(place)d place."
#: app/blueprints/users/profile.py:187
#, python-format
@ -1049,8 +1049,8 @@ msgstr "Fils"
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgstr "Rechercher %(type)s"
msgid "Search projects"
msgstr "Rechercher projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13
@ -1383,8 +1383,8 @@ msgstr "Gérez vos préférences"
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgstr "Il s'agit d'une notification « %(type)s »."
msgid "This is a 'projects' notification."
msgstr "Il s'agit d'une notification « projects »."
#: app/templates/emails/notification_digest.html:14
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgstr "Recommandez-vous ce %(type)s ?"
msgid "Do you recommend this projects?"
msgstr "Recommandez-vous ce projects ?"
#: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40
@ -1884,10 +1884,10 @@ msgstr "Lire plus"
#: app/templates/packages/create_edit.html:49
#, python-format
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."
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."
#: app/templates/packages/create_edit.html:55

View File

@ -786,17 +786,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgid "Top projects"
msgstr ""
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr ""
#: app/blueprints/users/profile.py:172
#, 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 ""
#: app/blueprints/users/profile.py:187
@ -1053,7 +1053,7 @@ msgstr ""
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgid "Search projects"
msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1377,7 +1377,7 @@ msgstr ""
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgid "This is a 'projects' notification."
msgstr ""
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgid "Do you recommend this projects?"
msgstr ""
#: app/templates/macros/reviews.html:124
@ -1851,7 +1851,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49
#, python-format
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."
msgstr ""

View File

@ -768,18 +768,18 @@ msgstr "Anda berada pada urutan %(place)s."
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgstr "%(type)s teratas"
msgid "Top projects"
msgstr "projects teratas"
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgstr "%(group)d %(type)s teratas"
msgid "Top %(group)d projects"
msgstr "%(group)d projects teratas"
#: app/blueprints/users/profile.py:172
#, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d."
msgstr "%(display_name)s memiliki sebuah %(type)s yang ada di urutan ke-%(place)d."
msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s memiliki sebuah projects yang ada di urutan ke-%(place)d."
#: app/blueprints/users/profile.py:187
#, python-format
@ -1043,8 +1043,8 @@ msgstr "Utas"
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgstr "Cari %(type)s"
msgid "Search projects"
msgstr "Cari projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13
@ -1373,8 +1373,8 @@ msgstr "Kelola pilihan Anda"
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgstr "Ini adalah pemberitahuan '%(type)s'."
msgid "This is a 'projects' notification."
msgstr "Ini adalah pemberitahuan 'projects'."
#: app/templates/emails/notification_digest.html:14
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgstr "Apa Anda menyarankan %(type)s ini?"
msgid "Do you recommend this projects?"
msgstr "Apa Anda menyarankan projects ini?"
#: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40
@ -1865,10 +1865,10 @@ msgstr "Baca lebih banyak"
#: app/templates/packages/create_edit.html:49
#, python-format
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."
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."
#: app/templates/packages/create_edit.html:55

View File

@ -748,17 +748,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgid "Top projects"
msgstr ""
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr ""
#: app/blueprints/users/profile.py:172
#, 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 ""
#: app/blueprints/users/profile.py:187
@ -1008,7 +1008,7 @@ msgstr ""
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgid "Search projects"
msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1332,7 +1332,7 @@ msgstr ""
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgid "This is a 'projects' notification."
msgstr ""
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgid "Do you recommend this projects?"
msgstr ""
#: app/templates/macros/reviews.html:124
@ -1806,7 +1806,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49
#, python-format
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."
msgstr ""

View File

@ -748,17 +748,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgid "Top projects"
msgstr ""
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr ""
#: app/blueprints/users/profile.py:172
#, 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 ""
#: app/blueprints/users/profile.py:187
@ -1008,7 +1008,7 @@ msgstr ""
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgid "Search projects"
msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1332,7 +1332,7 @@ msgstr ""
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgid "This is a 'projects' notification."
msgstr ""
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgid "Do you recommend this projects?"
msgstr ""
#: app/templates/macros/reviews.html:124
@ -1806,7 +1806,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49
#, python-format
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."
msgstr ""

View File

@ -774,18 +774,18 @@ msgstr "Anda berada di kedudukan %(place)s."
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgstr "%(type)s teratas"
msgid "Top projects"
msgstr "projects teratas"
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgstr "%(type)s %(group)d teratas"
msgid "Top %(group)d projects"
msgstr "projects %(group)d teratas"
#: app/blueprints/users/profile.py:172
#, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d."
msgstr "%(display_name)s mempunyai suatu %(type)s berkedudukan #%(place)d."
msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s mempunyai suatu projects berkedudukan #%(place)d."
#: app/blueprints/users/profile.py:187
#, python-format
@ -1049,8 +1049,8 @@ msgstr "Bebenang"
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgstr "Cari %(type)s"
msgid "Search projects"
msgstr "Cari projects"
#: app/templates/base.html:48 app/templates/todo/tags.html:11
#: app/templates/todo/tags.html:13
@ -1381,8 +1381,8 @@ msgstr "Uruskan keutamaan anda"
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgstr "Ini pemberitahuan '%(type)s'."
msgid "This is a 'projects' notification."
msgstr "Ini pemberitahuan 'projects'."
#: app/templates/emails/notification_digest.html:14
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgstr "Adakah anda mengesyorkan %(type)s ini?"
msgid "Do you recommend this projects?"
msgstr "Adakah anda mengesyorkan projects ini?"
#: app/templates/macros/reviews.html:124
#: app/templates/packages/review_create_edit.html:40
@ -1879,10 +1879,10 @@ msgstr "Baca lanjut"
#: app/templates/packages/create_edit.html:49
#, python-format
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."
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."
#: app/templates/packages/create_edit.html:55

View File

@ -750,17 +750,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgid "Top projects"
msgstr ""
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr ""
#: app/blueprints/users/profile.py:172
#, 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 ""
#: app/blueprints/users/profile.py:187
@ -1011,7 +1011,7 @@ msgstr ""
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgid "Search projects"
msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1335,7 +1335,7 @@ msgstr ""
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgid "This is a 'projects' notification."
msgstr ""
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgid "Do you recommend this projects?"
msgstr ""
#: app/templates/macros/reviews.html:124
@ -1809,7 +1809,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49
#, python-format
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."
msgstr ""

View File

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

View File

@ -750,17 +750,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgid "Top projects"
msgstr ""
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr ""
#: app/blueprints/users/profile.py:172
#, 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 ""
#: app/blueprints/users/profile.py:187
@ -1010,7 +1010,7 @@ msgstr ""
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgid "Search projects"
msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1334,7 +1334,7 @@ msgstr ""
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgid "This is a 'projects' notification."
msgstr ""
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgid "Do you recommend this projects?"
msgstr ""
#: app/templates/macros/reviews.html:124
@ -1808,7 +1808,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49
#, python-format
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."
msgstr ""

View File

@ -754,17 +754,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgid "Top projects"
msgstr ""
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr ""
#: app/blueprints/users/profile.py:172
#, 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 ""
#: app/blueprints/users/profile.py:187
@ -1014,7 +1014,7 @@ msgstr ""
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgid "Search projects"
msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1338,7 +1338,7 @@ msgstr ""
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgid "This is a 'projects' notification."
msgstr ""
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgid "Do you recommend this projects?"
msgstr ""
#: app/templates/macros/reviews.html:124
@ -1812,7 +1812,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49
#, python-format
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."
msgstr ""

View File

@ -752,18 +752,18 @@ msgstr "你在第%(place)s位。"
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgstr "最高%(type)s"
msgid "Top projects"
msgstr "最高projects"
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgstr "最高 %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr "最高 %(group)d projects"
#: app/blueprints/users/profile.py:172
#, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d."
msgstr "%(display_name)s 有一个 %(type)s 放置在 #%(place)d 处。"
msgid "%(display_name)s has a projects placed at #%(place)d."
msgstr "%(display_name)s 有一个 projects 放置在 #%(place)d 处。"
#: app/blueprints/users/profile.py:187
#, python-format
@ -1020,7 +1020,7 @@ msgstr ""
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgid "Search projects"
msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1358,7 +1358,7 @@ msgstr ""
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgid "This is a 'projects' notification."
msgstr ""
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgid "Do you recommend this projects?"
msgstr ""
#: app/templates/macros/reviews.html:124
@ -1840,7 +1840,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49
#, python-format
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."
msgstr ""

View File

@ -753,17 +753,17 @@ msgstr ""
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgid "Top projects"
msgstr ""
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgid "Top %(group)d projects"
msgstr ""
#: app/blueprints/users/profile.py:172
#, 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 ""
#: app/blueprints/users/profile.py:187
@ -1014,7 +1014,7 @@ msgstr ""
#: app/templates/base.html:48
#, python-format
msgid "Search %(type)s"
msgid "Search projects"
msgstr ""
#: app/templates/base.html:48 app/templates/todo/tags.html:11
@ -1338,7 +1338,7 @@ msgstr ""
#: app/templates/emails/notification.html:37
#, python-format
msgid "This is a '%(type)s' notification."
msgid "This is a 'projects' notification."
msgstr ""
#: 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/packages/review_create_edit.html:35
#, python-format
msgid "Do you recommend this %(type)s?"
msgid "Do you recommend this projects?"
msgstr ""
#: app/templates/macros/reviews.html:124
@ -1812,7 +1812,7 @@ msgstr ""
#: app/templates/packages/create_edit.html:49
#, python-format
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."
msgstr ""