Compare commits

...

12 Commits

174 changed files with 1881 additions and 4436 deletions

View File

@ -1,24 +1,22 @@
FROM python:3.10 FROM python:3.10
RUN groupadd -g 5123 cdb && \ RUN groupadd -g 5123 cdb && \
useradd -r -u 5123 -g cdb cdb useradd -r -u 5123 -g cdb cdb
WORKDIR /home/cdb WORKDIR /home/cdb
RUN mkdir /var/cdb RUN mkdir /var/cdb
RUN chown -R cdb:cdb /var/cdb RUN chown -R cdb:cdb /var/cdb
COPY requirements.lock.txt requirements.lock.txt COPY requirements.lock.txt requirements.lock.txt
RUN apt update
RUN apt install -y vim
RUN apt install -y libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev gir1.2-gtk-3.0
RUN apt install -y libappstream-glib-dev
RUN pip install -r requirements.lock.txt RUN pip install -r requirements.lock.txt
RUN pip install gunicorn RUN pip install gunicorn
RUN pip3 install pycairo PyGObject
COPY utils utils COPY utils utils
COPY config.cfg config.cfg COPY config.cfg config.cfg
COPY migrations migrations COPY migrations migrations
COPY app app COPY app app
COPY translations translations COPY translations translations
RUN pybabel compile -d translations RUN pybabel compile -d translations
RUN chown -R cdb:cdb /home/cdb RUN chown -R cdb:cdb /home/cdb
USER cdb
USER cdb

View File

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

View File

@ -22,42 +22,42 @@ from wtforms.validators import InputRequired, Length
from app.utils import rank_required from app.utils import rank_required
from . import bp from . import bp
from ...models import UserRank, MinetestRelease, db from ...models import UserRank, db
@bp.route("/versions/") # @bp.route("/versions/")
@rank_required(UserRank.MODERATOR) # @rank_required(UserRank.MODERATOR)
def version_list(): # def version_list():
return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all()) # return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
class VersionForm(FlaskForm): # class VersionForm(FlaskForm):
name = StringField("Name", [InputRequired(), Length(3, 100)]) # name = StringField("Name", [InputRequired(), Length(3, 100)])
protocol = IntegerField("Protocol") # protocol = IntegerField("Protocol")
submit = SubmitField("Save") # submit = SubmitField("Save")
@bp.route("/versions/new/", methods=["GET", "POST"]) # @bp.route("/versions/new/", methods=["GET", "POST"])
@bp.route("/versions/<name>/edit/", methods=["GET", "POST"]) # @bp.route("/versions/<name>/edit/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR) # @rank_required(UserRank.MODERATOR)
def create_edit_version(name=None): # def create_edit_version(name=None):
version = None # version = None
if name is not None: # if name is not None:
version = MinetestRelease.query.filter_by(name=name).first() # version = MinetestRelease.query.filter_by(name=name).first()
if version is None: # if version is None:
abort(404) # abort(404)
form = VersionForm(formdata=request.form, obj=version) # form = VersionForm(formdata=request.form, obj=version)
if form.validate_on_submit(): # if form.validate_on_submit():
if version is None: # if version is None:
version = MinetestRelease(form.name.data) # version = MinetestRelease(form.name.data)
db.session.add(version) # db.session.add(version)
flash("Created version " + form.name.data, "success") # flash("Created version " + form.name.data, "success")
else: # else:
flash("Updated version " + form.name.data, "success") # flash("Updated version " + form.name.data, "success")
form.populate_obj(version) # form.populate_obj(version)
db.session.commit() # db.session.commit()
return redirect(url_for("admin.version_list")) # return redirect(url_for("admin.version_list"))
return render_template("admin/versions/edit.html", version=version, form=form) # return render_template("admin/versions/edit.html", version=version, form=form)

View File

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

View File

@ -20,7 +20,7 @@ from flask import jsonify, abort, make_response, url_for, current_app
from app.logic.packages import do_edit_package from app.logic.packages import do_edit_package
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image
from app.models import APIToken, Package, MinetestRelease, PackageScreenshot from app.models import APIToken, Package, PackageScreenshot
def error(code: int, msg: str): def error(code: int, msg: str):
@ -38,7 +38,7 @@ def guard(f):
def api_create_vcs_release(token: APIToken, package: Package, title: str, ref: str, def api_create_vcs_release(token: APIToken, package: Package, title: str, ref: str,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API"): min_v = None, max_v = None, reason="API"):
if not token.canOperateOnPackage(package): if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package") error(403, "API token does not have access to the package")
@ -54,7 +54,7 @@ def api_create_vcs_release(token: APIToken, package: Package, title: str, ref: s
def api_create_zip_release(token: APIToken, package: Package, title: str, file, def api_create_zip_release(token: APIToken, package: Package, title: str, file,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API", commit_hash:str=None): min_v = None, max_v = None, reason="API", commit_hash:str=None):
if not token.canOperateOnPackage(package): if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package") error(403, "API token does not have access to the package")

View File

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

View File

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

View File

@ -99,10 +99,12 @@ def list_all():
selected_tags = set(qb.tags) selected_tags = set(qb.tags)
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
return render_template("packages/list.html", return render_template("packages/list.html",
query_hint=title, packages=query.items, pagination=query, query_hint=title, packages=query.items, pagination=query,
query=search, tags=tags, selected_tags=selected_tags, type=type_name, query=search, tags=tags, selected_tags=selected_tags, type=type_name,
authors=authors, packages_count=query.total, topics=topics) authors=authors, packages_count=query.total, topics=topics, toplevel=toplevel_tags)
def getReleases(package): def getReleases(package):
@ -120,30 +122,28 @@ def view(package):
package.checkPerm(current_user, Permission.APPROVE_NEW)) package.checkPerm(current_user, Permission.APPROVE_NEW))
conflicting_modnames = None conflicting_modnames = None
if show_similar and package.type != PackageType.TXP: if show_similar:
conflicting_modnames = db.session.query(MetaPackage.name) \ conflicting_modnames = db.session.query(MetaPackage.name) \
.filter(MetaPackage.id.in_([ mp.id for mp in package.provides ])) \ .filter(MetaPackage.id.in_([ mp.id for mp in package.provides ])) \
.filter(MetaPackage.packages.any(Package.id != package.id)) \ .filter(MetaPackage.packages.any(Package.id != package.id)) \
.all() .all()
conflicting_modnames += db.session.query(ForumTopic.name) \ # conflicting_modnames += db.session.query(ForumTopic.name) \
.filter(ForumTopic.name.in_([ mp.name for mp in package.provides ])) \ # .filter(ForumTopic.name.in_([ mp.name for mp in package.provides ])) \
.filter(ForumTopic.topic_id != package.forums) \ # .filter(ForumTopic.topic_id != package.forums) \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \ # .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \ # .order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all() # .all()
conflicting_modnames = set([x[0] for x in conflicting_modnames]) conflicting_modnames = set([x[0] for x in conflicting_modnames])
packages_uses = None packages_uses = None
if package.type == PackageType.MOD: packages_uses = Package.query.filter(
packages_uses = Package.query.filter( Package.id != package.id,
Package.type == PackageType.MOD, Package.state == PackageState.APPROVED,
Package.id != package.id, Package.dependencies.any(
Package.state == PackageState.APPROVED, Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
Package.dependencies.any( .order_by(db.desc(Package.score)).limit(6).all()
Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
.order_by(db.desc(Package.score)).limit(6).all()
releases = getReleases(package) releases = getReleases(package)
@ -153,21 +153,21 @@ def view(package):
topic_error = None topic_error = None
topic_error_lvl = "warning" topic_error_lvl = "warning"
if package.state != PackageState.APPROVED and package.forums is not None: # if package.state != PackageState.APPROVED and package.forums is not None:
errors = [] # errors = []
if Package.query.filter(Package.forums==package.forums, Package.state!=PackageState.DELETED).count() > 1: # if Package.query.filter(Package.forums==package.forums, Package.state!=PackageState.DELETED).count() > 1:
errors.append("<b>" + gettext("Error: Another package already uses this forum topic!") + "</b>") # errors.append("<b>" + gettext("Error: Another package already uses this forum topic!") + "</b>")
topic_error_lvl = "danger" # topic_error_lvl = "danger"
topic = ForumTopic.query.get(package.forums) # topic = ForumTopic.query.get(package.forums)
if topic is not None: # if topic is not None:
if topic.author != package.author: # if topic.author != package.author:
errors.append("<b>" + gettext("Error: Forum topic author doesn't match package author.") + "</b>") # errors.append("<b>" + gettext("Error: Forum topic author doesn't match package author.") + "</b>")
topic_error_lvl = "danger" # topic_error_lvl = "danger"
elif package.type != PackageType.TXP: # elif package.type != PackageType.ASSETPACK:
errors.append(gettext("Warning: Forum topic not found. This may happen if the topic has only just been created.")) # errors.append(gettext("Warning: Forum topic not found. This may happen if the topic has only just been created."))
topic_error = "<br />".join(errors) # topic_error = "<br />".join(errors)
threads = Thread.query.filter_by(package_id=package.id, review_id=None) threads = Thread.query.filter_by(package_id=package.id, review_id=None)
@ -178,11 +178,13 @@ def view(package):
has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0 has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
return render_template("packages/view.html", return render_template("packages/view.html",
package=package, releases=releases, packages_uses=packages_uses, package=package, releases=releases, packages_uses=packages_uses,
conflicting_modnames=conflicting_modnames, conflicting_modnames=conflicting_modnames,
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl,
threads=threads.all(), has_review=has_review) threads=threads.all(), has_review=has_review, toplevel=toplevel_tags)
@bp.route("/packages/<author>/<name>/shields/<type>/") @bp.route("/packages/<author>/<name>/shields/<type>/")
@ -226,9 +228,16 @@ def makeLabel(obj):
class PackageForm(FlaskForm): class PackageForm(FlaskForm):
type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD) try:
toplevel_tags = [ x.title for x in Tag.query.filter_by(is_toplevel=True).all() ]
type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=toplevel_tags, default=toplevel_tags[0])
except:
toplevel_tags = []
# type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=toplevel_tags, default=toplevel_tags[0])
# type = SelectField(lazy_gettext("Type"), [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.GAME)
title = StringField(lazy_gettext("Title (Human-readable)"), [InputRequired(), Length(1, 100)]) title = StringField(lazy_gettext("Title (Human-readable)"), [InputRequired(), Length(1, 100)])
name = StringField(lazy_gettext("Name (Technical)"), [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_) only"))]) name = StringField(lazy_gettext("Name (Technical)"), [InputRequired(), Length(1, 100), Regexp("^[a-zA-Z0-9_\-\.]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_), dashes and periods only"))])
short_desc = StringField(lazy_gettext("Short Description (Plaintext)"), [InputRequired(), Length(1,200)]) short_desc = StringField(lazy_gettext("Short Description (Plaintext)"), [InputRequired(), Length(1,200)])
dev_state = SelectField(lazy_gettext("Maintenance State"), [InputRequired()], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce) dev_state = SelectField(lazy_gettext("Maintenance State"), [InputRequired()], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce)
@ -243,7 +252,7 @@ class PackageForm(FlaskForm):
repo = StringField(lazy_gettext("VCS Repository URL"), [Optional(), URL()], filters = [lambda x: x or None]) repo = StringField(lazy_gettext("VCS Repository URL"), [Optional(), URL()], filters = [lambda x: x or None])
website = StringField(lazy_gettext("Website URL"), [Optional(), URL()], filters = [lambda x: x or None]) website = StringField(lazy_gettext("Website URL"), [Optional(), URL()], filters = [lambda x: x or None])
issueTracker = StringField(lazy_gettext("Issue Tracker URL"), [Optional(), URL()], filters = [lambda x: x or None]) issueTracker = StringField(lazy_gettext("Issue Tracker URL"), [Optional(), URL()], filters = [lambda x: x or None])
forums = IntegerField(lazy_gettext("Forum Topic ID"), [Optional(), NumberRange(0,999999)]) # forums = IntegerField(lazy_gettext("Forum Topic ID"), [Optional(), NumberRange(0,999999)])
video_url = StringField(lazy_gettext("Video URL"), [Optional(), URL()], filters = [lambda x: x or None]) video_url = StringField(lazy_gettext("Video URL"), [Optional(), URL()], filters = [lambda x: x or None])
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
@ -286,16 +295,13 @@ def create_edit(author=None, name=None):
form.name.data = request.args.get("bname") form.name.data = request.args.get("bname")
form.title.data = request.args.get("title") form.title.data = request.args.get("title")
form.repo.data = request.args.get("repo") form.repo.data = request.args.get("repo")
form.forums.data = request.args.get("forums") # form.forums.data = request.args.get("forums")
form.license.data = None form.license.data = None
form.media_license.data = None form.media_license.data = None
else: else:
form.tags.data = package.tags form.tags.data = package.tags
form.content_warnings.data = package.content_warnings form.content_warnings.data = package.content_warnings
if request.method == "POST" and form.type.data == PackageType.TXP:
form.license.data = form.media_license.data
if form.validate_on_submit(): if form.validate_on_submit():
wasNew = False wasNew = False
if not package: if not package:
@ -327,7 +333,7 @@ def create_edit(author=None, name=None):
"repo": form.repo.data, "repo": form.repo.data,
"website": form.website.data, "website": form.website.data,
"issueTracker": form.issueTracker.data, "issueTracker": form.issueTracker.data,
"forums": form.forums.data, # "forums": form.forums.data,
"video_url": form.video_url.data, "video_url": form.video_url.data,
}) })
@ -353,7 +359,7 @@ def create_edit(author=None, name=None):
form=form, author=author, enable_wizard=enableWizard, form=form, author=author, enable_wizard=enableWizard,
packages=package_query.all(), packages=package_query.all(),
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all(), mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all(),
tabs=get_package_tabs(current_user, package), current_tab="edit") tabs=get_package_tabs(current_user, package), current_tab="edit", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/state/", methods=["POST"]) @bp.route("/packages/<author>/<name>/state/", methods=["POST"])
@ -408,7 +414,7 @@ def move_to_state(package):
def remove(package): def remove(package):
if request.method == "GET": if request.method == "GET":
return render_template("packages/remove.html", package=package, return render_template("packages/remove.html", package=package,
tabs=get_package_tabs(current_user, package), current_tab="remove") tabs=get_package_tabs(current_user, package), current_tab="remove", toplevel=get_toplevel_tags())
reason = request.form.get("reason") or "?" reason = request.form.get("reason") or "?"
@ -501,7 +507,7 @@ def edit_maintainers(package):
users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).order_by(db.asc(User.username)).all() users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).order_by(db.asc(User.username)).all()
return render_template("packages/edit_maintainers.html", package=package, form=form, return render_template("packages/edit_maintainers.html", package=package, form=form,
users=users, tabs=get_package_tabs(current_user, package), current_tab="maintainers") users=users, tabs=get_package_tabs(current_user, package), current_tab="maintainers", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/remove-self-maintainer/", methods=["POST"]) @bp.route("/packages/<author>/<name>/remove-self-maintainer/", methods=["POST"])
@ -540,13 +546,13 @@ def audit(package):
pagination = query.paginate(page, num, True) pagination = query.paginate(page, num, True)
return render_template("packages/audit.html", log=pagination.items, pagination=pagination, return render_template("packages/audit.html", log=pagination.items, pagination=pagination,
package=package, tabs=get_package_tabs(current_user, package), current_tab="audit") package=package, tabs=get_package_tabs(current_user, package), current_tab="audit", toplevel=get_toplevel_tags())
class PackageAliasForm(FlaskForm): class PackageAliasForm(FlaskForm):
author = StringField(lazy_gettext("Author Name"), [InputRequired(), Length(1, 50)]) author = StringField(lazy_gettext("Author Name"), [InputRequired(), Length(1, 50)])
name = StringField(lazy_gettext("Name (Technical)"), [InputRequired(), Length(1, 100), name = StringField(lazy_gettext("Name (Technical)"), [InputRequired(), Length(1, 100),
Regexp("^[a-z0-9_]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_) only"))]) Regexp("^[a-zA-Z0-9_\-\.]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_), dashes and periods only"))])
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
@ -554,7 +560,7 @@ class PackageAliasForm(FlaskForm):
@rank_required(UserRank.EDITOR) @rank_required(UserRank.EDITOR)
@is_package_page @is_package_page
def alias_list(package: Package): def alias_list(package: Package):
return render_template("packages/alias_list.html", package=package) return render_template("packages/alias_list.html", package=package, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/aliases/new/", methods=["GET", "POST"]) @bp.route("/packages/<author>/<name>/aliases/new/", methods=["GET", "POST"])
@ -580,7 +586,7 @@ def alias_create_edit(package: Package, alias_id: int = None):
return redirect(package.getURL("packages.alias_list")) return redirect(package.getURL("packages.alias_list"))
return render_template("packages/alias_create_edit.html", package=package, form=form) return render_template("packages/alias_create_edit.html", package=package, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/share/") @bp.route("/packages/<author>/<name>/share/")
@ -588,7 +594,7 @@ def alias_create_edit(package: Package, alias_id: int = None):
@is_package_page @is_package_page
def share(package): def share(package):
return render_template("packages/share.html", package=package, return render_template("packages/share.html", package=package,
tabs=get_package_tabs(current_user, package), current_tab="share") tabs=get_package_tabs(current_user, package), current_tab="share", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/similar/") @bp.route("/packages/<author>/<name>/similar/")
@ -602,12 +608,12 @@ def similar(package):
.order_by(db.desc(Package.score)) \ .order_by(db.desc(Package.score)) \
.all() .all()
similar_topics = ForumTopic.query \ # similar_topics = ForumTopic.query \
.filter_by(name=package.name) \ # .filter_by(name=package.name) \
.filter(ForumTopic.topic_id != package.forums) \ # .filter(ForumTopic.topic_id != package.forums) \
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \ # .filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \ # .order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all() # .all()
return render_template("packages/similar.html", package=package, return render_template("packages/similar.html", package=package,
packages_modnames=packages_modnames, similar_topics=similar_topics) packages_modnames=packages_modnames, similar_topics=[], toplevel=get_toplevel_tags())

View File

@ -35,17 +35,17 @@ from . import bp, get_package_tabs
def list_releases(package): def list_releases(package):
return render_template("packages/releases_list.html", return render_template("packages/releases_list.html",
package=package, package=package,
tabs=get_package_tabs(current_user, package), current_tab="releases") tabs=get_package_tabs(current_user, package), current_tab="releases", toplevel=get_toplevel_tags())
def get_mt_releases(is_max): # def get_mt_releases(is_max):
query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id)) # query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
if is_max: # if is_max:
query = query.limit(query.count() - 1) # query = query.limit(query.count() - 1)
else: # else:
query = query.filter(MinetestRelease.name != "0.4.17") # query = query.filter(MinetestRelease.name != "0.4.17")
return query # return query
class CreatePackageReleaseForm(FlaskForm): class CreatePackageReleaseForm(FlaskForm):
@ -53,10 +53,6 @@ class CreatePackageReleaseForm(FlaskForm):
uploadOpt = RadioField(lazy_gettext("Method"), choices=[("upload", lazy_gettext("File Upload"))], default="upload") uploadOpt = RadioField(lazy_gettext("Method"), choices=[("upload", lazy_gettext("File Upload"))], default="upload")
vcsLabel = StringField(lazy_gettext("Git reference (ie: commit hash, branch, or tag)"), default=None) vcsLabel = StringField(lazy_gettext("Git reference (ie: commit hash, branch, or tag)"), default=None)
fileUpload = FileField(lazy_gettext("File Upload")) fileUpload = FileField(lazy_gettext("File Upload"))
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
@ -65,10 +61,6 @@ class EditPackageReleaseForm(FlaskForm):
url = StringField(lazy_gettext("URL"), [Optional()]) url = StringField(lazy_gettext("URL"), [Optional()])
task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None]) task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None])
approved = BooleanField(lazy_gettext("Is Approved")) approved = BooleanField(lazy_gettext("Is Approved"))
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
@ -93,16 +85,14 @@ def create_release(package):
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
if form["uploadOpt"].data == "vcs": if form["uploadOpt"].data == "vcs":
rel = do_create_vcs_release(current_user, package, form.title.data, rel = do_create_vcs_release(current_user, package, form.title.data, form.vcsLabel.data)
form.vcsLabel.data, form.min_rel.data.getActual(), form.max_rel.data.getActual())
else: else:
rel = do_create_zip_release(current_user, package, form.title.data, rel = do_create_zip_release(current_user, package, form.title.data, form.fileUpload.data)
form.fileUpload.data, form.min_rel.data.getActual(), form.max_rel.data.getActual())
return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL())) return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL()))
except LogicError as e: except LogicError as e:
flash(e.message, "danger") flash(e.message, "danger")
return render_template("packages/release_new.html", package=package, form=form) return render_template("packages/release_new.html", package=package, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/releases/<id>/download/") @bp.route("/packages/<author>/<name>/releases/<id>/download/")
@ -158,8 +148,6 @@ def edit_release(package, id):
if form.validate_on_submit(): if form.validate_on_submit():
if canEdit: if canEdit:
release.title = form["title"].data release.title = form["title"].data
release.min_rel = form["min_rel"].data.getActual()
release.max_rel = form["max_rel"].data.getActual()
if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL): if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
release.url = form["url"].data release.url = form["url"].data
@ -175,19 +163,19 @@ def edit_release(package, id):
db.session.commit() db.session.commit()
return redirect(package.getURL("packages.list_releases")) return redirect(package.getURL("packages.list_releases"))
return render_template("packages/release_edit.html", package=package, release=release, form=form) return render_template("packages/release_edit.html", package=package, release=release, form=form, toplevel=get_toplevel_tags())
class BulkReleaseForm(FlaskForm): # class BulkReleaseForm(FlaskForm):
set_min = BooleanField(lazy_gettext("Set Min")) # set_min = BooleanField(lazy_gettext("Set Min"))
min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()], # min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name) # query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
set_max = BooleanField(lazy_gettext("Set Max")) # set_max = BooleanField(lazy_gettext("Set Max"))
max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()], # max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name) # query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
only_change_none = BooleanField(lazy_gettext("Only change values previously set as none")) # only_change_none = BooleanField(lazy_gettext("Only change values previously set as none"))
submit = SubmitField(lazy_gettext("Update")) # submit = SubmitField(lazy_gettext("Update"))
@bp.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"]) @bp.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"])
@ -205,17 +193,17 @@ def bulk_change_release(package):
elif form.validate_on_submit(): elif form.validate_on_submit():
only_change_none = form.only_change_none.data only_change_none = form.only_change_none.data
for release in package.releases.all(): # for release in package.releases.all():
if form["set_min"].data and (not only_change_none or release.min_rel is None): # if form["set_min"].data and (not only_change_none or release.min_rel is None):
release.min_rel = form["min_rel"].data.getActual() # release.min_rel = form["min_rel"].data.getActual()
if form["set_max"].data and (not only_change_none or release.max_rel is None): # if form["set_max"].data and (not only_change_none or release.max_rel is None):
release.max_rel = form["max_rel"].data.getActual() # release.max_rel = form["max_rel"].data.getActual()
db.session.commit() db.session.commit()
return redirect(package.getURL("packages.list_releases")) return redirect(package.getURL("packages.list_releases"))
return render_template("packages/release_bulk_change.html", package=package, form=form) return render_template("packages/release_bulk_change.html", package=package, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/releases/<id>/delete/", methods=["POST"]) @bp.route("/packages/<author>/<name>/releases/<id>/delete/", methods=["POST"])
@ -313,7 +301,7 @@ def update_config(package):
return redirect(package.getURL("packages.list_releases")) return redirect(package.getURL("packages.list_releases"))
return render_template("packages/update_config.html", package=package, form=form) return render_template("packages/update_config.html", package=package, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/setup-releases/") @bp.route("/packages/<author>/<name>/setup-releases/")
@ -326,7 +314,7 @@ def setup_releases(package):
if package.update_config: if package.update_config:
return redirect(package.getURL("packages.update_config")) return redirect(package.getURL("packages.update_config"))
return render_template("packages/release_wizard.html", package=package) return render_template("packages/release_wizard.html", package=package, toplevel=get_toplevel_tags())
@bp.route("/user/update-configs/") @bp.route("/user/update-configs/")
@ -355,4 +343,4 @@ def bulk_update_config(username=None):
Package.update_config.has()) \ Package.update_config.has()) \
.order_by(db.asc(Package.title)).all() .order_by(db.asc(Package.title)).all()
return render_template("packages/bulk_update_conf.html", user=user, confs=confs, form=form) return render_template("packages/bulk_update_conf.html", user=user, confs=confs, form=form, toplevel=get_toplevel_tags())

View File

@ -26,7 +26,7 @@ from wtforms import *
from wtforms.validators import * from wtforms.validators import *
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \ from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \
Permission, AuditSeverity Permission, AuditSeverity
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog, get_toplevel_tags
from app.tasks.webhooktasks import post_discord_webhook from app.tasks.webhooktasks import post_discord_webhook
@ -36,7 +36,7 @@ def list_reviews():
num = min(40, get_int_or_abort(request.args.get("n"), 100)) num = min(40, get_int_or_abort(request.args.get("n"), 100))
pagination = PackageReview.query.order_by(db.desc(PackageReview.created_at)).paginate(page, num, True) pagination = PackageReview.query.order_by(db.desc(PackageReview.created_at)).paginate(page, num, True)
return render_template("packages/reviews_list.html", pagination=pagination, reviews=pagination.items) return render_template("packages/reviews_list.html", pagination=pagination, reviews=pagination.items, toplevel=get_toplevel_tags())
class ReviewForm(FlaskForm): class ReviewForm(FlaskForm):
@ -123,7 +123,7 @@ def review(package):
return redirect(package.getURL("packages.view")) return redirect(package.getURL("packages.view"))
return render_template("packages/review_create_edit.html", return render_template("packages/review_create_edit.html",
form=form, package=package, review=review) form=form, package=package, review=review, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/reviews/<reviewer>/delete/", methods=["POST"]) @bp.route("/packages/<author>/<name>/reviews/<reviewer>/delete/", methods=["POST"])
@ -237,4 +237,4 @@ def review_votes(package):
user_biases_info.sort(key=lambda x: -abs(x.balance)) user_biases_info.sort(key=lambda x: -abs(x.balance))
return render_template("packages/review_votes.html", form=form, package=package, reviews=package.reviews, return render_template("packages/review_votes.html", form=form, package=package, reviews=package.reviews,
user_biases=user_biases_info) user_biases=user_biases_info, toplevel=get_toplevel_tags())

View File

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

View File

@ -15,7 +15,9 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import base64
import string
import random
from flask import * from flask import *
from flask_babel import gettext, lazy_gettext, get_locale from flask_babel import gettext, lazy_gettext, get_locale
from flask_login import current_user, login_required, logout_user, login_user from flask_login import current_user, login_required, logout_user, login_user
@ -23,6 +25,7 @@ from flask_wtf import FlaskForm
from sqlalchemy import or_ from sqlalchemy import or_
from wtforms import * from wtforms import *
from wtforms.validators import * from wtforms.validators import *
from captcha.image import ImageCaptcha
from app.models import * from app.models import *
from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email
@ -105,13 +108,13 @@ class RegisterForm(FlaskForm):
Regexp("^[a-zA-Z0-9._-]+$", message=lazy_gettext("Only a-zA-Z0-9._ allowed"))]) Regexp("^[a-zA-Z0-9._-]+$", message=lazy_gettext("Only a-zA-Z0-9._ allowed"))])
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()]) email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(6, 100)]) password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(6, 100)])
question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()]) question = StringField(lazy_gettext("Type the characters from the image above."), [InputRequired()])
agree = BooleanField(lazy_gettext("I agree"), [DataRequired()]) agree = BooleanField(lazy_gettext("I agree"), [DataRequired()])
submit = SubmitField(lazy_gettext("Register")) submit = SubmitField(lazy_gettext("Register"))
def handle_register(form): def handle_register(form):
if form.question.data.strip().lower() != "19": if form.question.data.strip().lower() != session["captcha-solution"]:
flash(gettext("Incorrect captcha answer"), "danger") flash(gettext("Incorrect captcha answer"), "danger")
return return
@ -181,7 +184,13 @@ def register():
if ret: if ret:
return ret return ret
return render_template("users/register.html", form=form, image = ImageCaptcha()
characters = string.ascii_letters + string.digits
solution = ''.join(random.choice(characters) for i in range(4))
data = image.generate(solution)
session["captcha-solution"] = solution
b64data = "data:image/png;base64," + base64.b64encode(data.read()).decode("utf-8")
return render_template("users/register.html", form=form, captcha=b64data,
suggested_password=genphrase(entropy=52, wordset="bip39")) suggested_password=genphrase(entropy=52, wordset="bip39"))

View File

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

View File

@ -1,37 +1,72 @@
import string
import random
from .models import * from .models import *
from .utils import make_flask_login_password from .utils import make_flask_login_password
def generate_password():
characters = string.ascii_letters + string.digits + string.punctuation
password = ''.join(random.choice(characters) for i in range(16))
return password
def populate(session): def populate(session):
admin_user = User("rubenwardy") admin_user = User("libregaming")
admin_user.is_active = True admin_user.is_active = True
admin_user.password = make_flask_login_password("tuckfrump") password = generate_password()
admin_user.github_username = "rubenwardy" admin_user.password = make_flask_login_password(password)
admin_user.forums_username = "rubenwardy" admin_user.github_username = "libregaming"
admin_user.forums_username = "libregaming"
admin_user.rank = UserRank.ADMIN admin_user.rank = UserRank.ADMIN
session.add(admin_user) session.add(admin_user)
print("#####################################")
print("Admin user : libregaming")
print("Admin password: " + password)
print("#####################################")
system_user = User("ContentDB", active=False) system_user = User("ContentDB", active=False)
system_user.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000) system_user.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
system_user.rank = UserRank.BOT system_user.rank = UserRank.BOT
session.add(system_user) session.add(system_user)
session.add(MinetestRelease("None", 0)) appstream_user = User("AppStreamBot", active=False)
session.add(MinetestRelease("0.4.16/17", 32)) appstream_user.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
session.add(MinetestRelease("5.0", 37)) appstream_user.rank = UserRank.BOT
session.add(MinetestRelease("5.1", 38)) session.add(appstream_user)
session.add(MinetestRelease("5.2", 39))
session.add(MinetestRelease("5.3", 39)) featured = Tag("featured")
featured.is_protected = True
# These tags replace "package types"
game_tag = Tag("Games")
game_tag.is_toplevel = True
tool_tag = Tag("Tools")
tool_tag.is_toplevel = True
mod_tag = Tag("Mods")
mod_tag.is_toplevel = True
session.add(featured)
session.add(game_tag)
session.add(tool_tag)
session.add(mod_tag)
tags = {} tags = {}
for tag in ["Inventory", "Mapgen", "Building", for tag in [
"Mobs and NPCs", "Tools", "Player effects", "Action",
"Environment", "Transport", "Maintenance", "Plants and farming", "Adventure",
"PvP", "PvE", "Survival", "Creative", "Puzzle", "Multiplayer", "Singleplayer", "Featured"]: "Arcade",
"Board",
"Blocks",
"Card",
"Kids",
"Logic",
"RolePlaying",
"Shooter",
"Simulation",
"Sports",
"Strategy"
]:
row = Tag(tag) row = Tag(tag)
tags[row.name] = row tags[row.name] = row
session.add(row) session.add(row)
tags["featured"] = featured
licenses = {} licenses = {}
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3", for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA", "Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
@ -50,9 +85,9 @@ def populate_test_data(session):
licenses = { x.name : x for x in License.query.all() } licenses = { x.name : x for x in License.query.all() }
tags = { x.name : x for x in Tag.query.all() } tags = { x.name : x for x in Tag.query.all() }
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first() admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
v4 = MinetestRelease.query.filter_by(protocol=32).first() # v4 = MinetestRelease.query.filter_by(protocol=32).first()
v50 = MinetestRelease.query.filter_by(protocol=37).first() # v50 = MinetestRelease.query.filter_by(protocol=37).first()
v51 = MinetestRelease.query.filter_by(protocol=38).first() # v51 = MinetestRelease.query.filter_by(protocol=38).first()
ez = User("Shara") ez = User("Shara")
ez.github_username = "Ezhh" ez.github_username = "Ezhh"
@ -60,7 +95,7 @@ def populate_test_data(session):
ez.rank = UserRank.EDITOR ez.rank = UserRank.EDITOR
session.add(ez) session.add(ez)
not1 = Notification(admin_user, ez, NotificationType.PACKAGE_APPROVAL, "Awards approved", "/packages/rubenwardy/awards/") not1 = Notification(admin_user, ez, NotificationType.PACKAGE_APPROVAL, "Awards approved", "/packages/libregaming/awards/")
session.add(not1) session.add(not1)
jeija = User("Jeija") jeija = User("Jeija")
@ -69,25 +104,25 @@ def populate_test_data(session):
session.add(jeija) session.add(jeija)
mod = Package() tool = Package()
mod.state = PackageState.APPROVED tool.state = PackageState.APPROVED
mod.name = "alpha" tool.name = "alpha"
mod.title = "Alpha Test" tool.title = "Alpha Test"
mod.license = licenses["MIT"] tool.license = licenses["MIT"]
mod.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
mod.type = PackageType.MOD # tool.type = PackageType.TOOL
mod.author = admin_user tool.author = admin_user
mod.tags.append(tags["mapgen"]) tool.tags.append(tags["mapgen"])
mod.tags.append(tags["environment"]) tool.tags.append(tags["environment"])
mod.repo = "https://github.com/ezhh/other_worlds" tool.repo = "https://github.com/ezhh/other_worlds"
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues" tool.issueTracker = "https://github.com/ezhh/other_worlds/issues"
mod.forums = 16015 tool.forums = 16015
mod.short_desc = "The content library should not be used yet as it is still in alpha" tool.short_desc = "The content library should not be used yet as it is still in alpha"
mod.desc = "This is the long desc" tool.desc = "This is the long desc"
session.add(mod) session.add(tool)
rel = PackageRelease() rel = PackageRelease()
rel.package = mod rel.package = tool
rel.title = "v1.0.0" rel.title = "v1.0.0"
rel.url = "https://github.com/ezhh/handholds/archive/master.zip" rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
rel.approved = True rel.approved = True
@ -99,11 +134,11 @@ def populate_test_data(session):
mod1.title = "Awards" mod1.title = "Awards"
mod1.license = licenses["LGPLv2.1"] mod1.license = licenses["LGPLv2.1"]
mod1.media_license = licenses["MIT"] mod1.media_license = licenses["MIT"]
mod1.type = PackageType.MOD # mod1.type = PackageType.TOOL
mod1.author = admin_user mod1.author = admin_user
mod1.tags.append(tags["player_effects"]) mod1.tags.append(tags["player_effects"])
mod1.repo = "https://github.com/rubenwardy/awards" mod1.repo = "https://github.com/libregaming/awards"
mod1.issueTracker = "https://github.com/rubenwardy/awards/issues" mod1.issueTracker = "https://github.com/libregaming/awards/issues"
mod1.forums = 4870 mod1.forums = 4870
mod1.short_desc = "Adds achievements and an API to register new ones." mod1.short_desc = "Adds achievements and an API to register new ones."
mod1.desc = """ mod1.desc = """
@ -126,7 +161,7 @@ awards.register_achievement("award_mesefind",{
rel.package = mod1 rel.package = mod1
rel.min_rel = v51 rel.min_rel = v51
rel.title = "v1.0.0" rel.title = "v1.0.0"
rel.url = "https://github.com/rubenwardy/awards/archive/master.zip" rel.url = "https://github.com/libregaming/awards/archive/master.zip"
rel.approved = True rel.approved = True
session.add(rel) session.add(rel)
@ -135,7 +170,7 @@ awards.register_achievement("award_mesefind",{
mod2.name = "mesecons" mod2.name = "mesecons"
mod2.title = "Mesecons" mod2.title = "Mesecons"
mod2.tags.append(tags["tools"]) mod2.tags.append(tags["tools"])
mod2.type = PackageType.MOD # mod2.type = PackageType.TOOL
mod2.license = licenses["LGPLv3"] mod2.license = licenses["LGPLv3"]
mod2.media_license = licenses["MIT"] mod2.media_license = licenses["MIT"]
mod2.author = jeija mod2.author = jeija
@ -150,7 +185,7 @@ Mezzee-what?
------------ ------------
[Mesecons](http://mesecons.net/)! They're yellow, they're conductive, and they'll add a whole new dimension to Minetest's gameplay. [Mesecons](http://mesecons.net/)! They're yellow, they're conductive, and they'll add a whole new dimension to Minetest's gameplay.
Mesecons is a mod for [Minetest](http://minetest.net/) that implements a ton of items related to digital circuitry, such as wires, buttons, lights, and even programmable controllers. Among other things, there are also pistons, solar panels, pressure plates, and note blocks. Mesecons is a tool for [Minetest](http://minetest.net/) that implements a ton of items related to digital circuitry, such as wires, buttons, lights, and even programmable controllers. Among other things, there are also pistons, solar panels, pressure plates, and note blocks.
Mesecons has a similar goal to Redstone in Minecraft, but works in its own way, with different rules and mechanics. Mesecons has a similar goal to Redstone in Minecraft, but works in its own way, with different rules and mechanics.
@ -160,14 +195,14 @@ Go get it!
[DOWNLOAD IT NOW](https://github.com/minetest-mods/mesecons/archive/master.zip) [DOWNLOAD IT NOW](https://github.com/minetest-mods/mesecons/archive/master.zip)
Now go ahead and install it like any other Minetest mod. Don't know how? Check out [the wonderful page about it](http://wiki.minetest.com/wiki/Mods) over at the Minetest Wiki. For your convenience, here's a quick summary: Now go ahead and install it like any other Minetest tool. Don't know how? Check out [the wonderful page about it](http://wiki.minetest.com/wiki/Tools) over at the Minetest Wiki. For your convenience, here's a quick summary:
1. If Mesecons is still in a ZIP file, extract the folder inside to somewhere on the computer. 1. If Mesecons is still in a ZIP file, extract the folder inside to somewhere on the computer.
2. Make sure that when you open the folder, you can directly find `README.md` in the listing. If you just see another folder, move that folder up one level and delete the old one. 2. Make sure that when you open the folder, you can directly find `README.md` in the listing. If you just see another folder, move that folder up one level and delete the old one.
3. Open up the Minetest mods folder - usually `/mods/`. If you see the `minetest` or folder inside of that, that is your mod folder instead. 3. Open up the Minetest mods folder - usually `/mods/`. If you see the `minetest` or folder inside of that, that is your tool folder instead.
4. Copy the Mesecons folder into the mods folder. 4. Copy the Mesecons folder into the mods folder.
Don't like some parts of Mesecons? Open up the Mesecons folder and delete the subfolder containing the mod you don't want. If you didn't want movestones, for example, all you have to do is delete the `mesecons_movestones` folder and they will no longer be available. Don't like some parts of Mesecons? Open up the Mesecons folder and delete the subfolder containing the tool you don't want. If you didn't want movestones, for example, all you have to do is delete the `mesecons_movestones` folder and they will no longer be available.
There are no dependencies - it will work right after installing! There are no dependencies - it will work right after installing!
@ -219,97 +254,97 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod1) session.add(mod1)
session.add(mod2) session.add(mod2)
mod = Package() tool = Package()
mod.state = PackageState.APPROVED tool.state = PackageState.APPROVED
mod.name = "handholds" tool.name = "handholds"
mod.title = "Handholds" tool.title = "Handholds"
mod.license = licenses["MIT"] tool.license = licenses["MIT"]
mod.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
mod.type = PackageType.MOD # tool.type = PackageType.TOOL
mod.author = ez tool.author = ez
mod.tags.append(tags["player_effects"]) tool.tags.append(tags["player_effects"])
mod.repo = "https://github.com/ezhh/handholds" tool.repo = "https://github.com/ezhh/handholds"
mod.issueTracker = "https://github.com/ezhh/handholds/issues" tool.issueTracker = "https://github.com/ezhh/handholds/issues"
mod.forums = 17069 tool.forums = 17069
mod.short_desc = "Adds hand holds and climbing thingies" tool.short_desc = "Adds hand holds and climbing thingies"
mod.desc = "This is the long desc" tool.desc = "This is the long desc"
session.add(mod) session.add(tool)
rel = PackageRelease() rel = PackageRelease()
rel.package = mod rel.package = tool
rel.title = "v1.0.0" rel.title = "v1.0.0"
rel.max_rel = v4 rel.max_rel = v4
rel.url = "https://github.com/ezhh/handholds/archive/master.zip" rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
rel.approved = True rel.approved = True
session.add(rel) session.add(rel)
mod = Package() tool = Package()
mod.state = PackageState.APPROVED tool.state = PackageState.APPROVED
mod.name = "other_worlds" tool.name = "other_worlds"
mod.title = "Other Worlds" tool.title = "Other Worlds"
mod.license = licenses["MIT"] tool.license = licenses["MIT"]
mod.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
mod.type = PackageType.MOD # tool.type = PackageType.TOOL
mod.author = ez tool.author = ez
mod.tags.append(tags["mapgen"]) tool.tags.append(tags["mapgen"])
mod.tags.append(tags["environment"]) tool.tags.append(tags["environment"])
mod.repo = "https://github.com/ezhh/other_worlds" tool.repo = "https://github.com/ezhh/other_worlds"
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues" tool.issueTracker = "https://github.com/ezhh/other_worlds/issues"
mod.forums = 16015 tool.forums = 16015
mod.short_desc = "Adds space with asteroids and comets" tool.short_desc = "Adds space with asteroids and comets"
mod.desc = "This is the long desc" tool.desc = "This is the long desc"
session.add(mod) session.add(tool)
mod = Package() tool = Package()
mod.state = PackageState.APPROVED tool.state = PackageState.APPROVED
mod.name = "food" tool.name = "food"
mod.title = "Food" tool.title = "Food"
mod.license = licenses["LGPLv2.1"] tool.license = licenses["LGPLv2.1"]
mod.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
mod.type = PackageType.MOD # tool.type = PackageType.TOOL
mod.author = admin_user tool.author = admin_user
mod.tags.append(tags["player_effects"]) tool.tags.append(tags["player_effects"])
mod.repo = "https://github.com/rubenwardy/food/" tool.repo = "https://github.com/libregaming/food/"
mod.issueTracker = "https://github.com/rubenwardy/food/issues/" tool.issueTracker = "https://github.com/libregaming/food/issues/"
mod.forums = 2960 tool.forums = 2960
mod.short_desc = "Adds lots of food and an API to manage ingredients" tool.short_desc = "Adds lots of food and an API to manage ingredients"
mod.desc = "This is the long desc" tool.desc = "This is the long desc"
session.add(mod) session.add(tool)
mod = Package() tool = Package()
mod.state = PackageState.APPROVED tool.state = PackageState.APPROVED
mod.name = "food_sweet" tool.name = "food_sweet"
mod.title = "Sweet Foods" tool.title = "Sweet Foods"
mod.license = licenses["CC0"] tool.license = licenses["CC0"]
mod.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
mod.type = PackageType.MOD # tool.type = PackageType.TOOL
mod.author = admin_user tool.author = admin_user
mod.tags.append(tags["player_effects"]) tool.tags.append(tags["player_effects"])
mod.repo = "https://github.com/rubenwardy/food_sweet/" tool.repo = "https://github.com/libregaming/food_sweet/"
mod.issueTracker = "https://github.com/rubenwardy/food_sweet/issues/" tool.issueTracker = "https://github.com/libregaming/food_sweet/issues/"
mod.forums = 9039 tool.forums = 9039
mod.short_desc = "Adds sweet food" tool.short_desc = "Adds sweet food"
mod.desc = "This is the long desc" tool.desc = "This is the long desc"
food_sweet = mod food_sweet = tool
session.add(mod) session.add(tool)
game1 = Package() game1 = Package()
game1.state = PackageState.APPROVED game1.state = PackageState.APPROVED
game1.name = "capturetheflag" game1.name = "capturetheflag"
game1.title = "Capture The Flag" game1.title = "Capture The Flag"
game1.type = PackageType.GAME # game1.type = PackageType.GAME
game1.license = licenses["LGPLv2.1"] game1.license = licenses["LGPLv2.1"]
game1.media_license = licenses["MIT"] game1.media_license = licenses["MIT"]
game1.author = admin_user game1.author = admin_user
game1.tags.append(tags["pvp"]) game1.tags.append(tags["pvp"])
game1.tags.append(tags["survival"]) game1.tags.append(tags["survival"])
game1.tags.append(tags["multiplayer"]) game1.tags.append(tags["multiplayer"])
game1.repo = "https://github.com/rubenwardy/capturetheflag" game1.repo = "https://github.com/libregaming/capturetheflag"
game1.issueTracker = "https://github.com/rubenwardy/capturetheflag/issues" game1.issueTracker = "https://github.com/libregaming/capturetheflag/issues"
game1.forums = 12835 game1.forums = 12835
game1.short_desc = "Two teams battle to snatch and return the enemy's flag, before the enemy takes their own!" game1.short_desc = "Two teams battle to snatch and return the enemy's flag, before the enemy takes their own!"
game1.desc = """ game1.desc = """
As seen on the Capture the Flag server (minetest.rubenwardy.com:30000) As seen on the Capture the Flag server (minetest.libregaming.com:30000)
` `[`javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/"/+/onmouseover=1/+/`](javascript:/*--%3E%3C/title%3E%3C/style%3E%3C/textarea%3E%3C/script%3E%3C/xmp%3E%3Csvg/onload='+/%22/+/onmouseover=1/+/)`[*/[]/+alert(1)//'>` ` `[`javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/"/+/onmouseover=1/+/`](javascript:/*--%3E%3C/title%3E%3C/style%3E%3C/textarea%3E%3C/script%3E%3C/xmp%3E%3Csvg/onload='+/%22/+/onmouseover=1/+/)`[*/[]/+alert(1)//'>`
@ -351,26 +386,26 @@ Uses the CTF PvP Engine.
rel = PackageRelease() rel = PackageRelease()
rel.package = game1 rel.package = game1
rel.title = "v1.0.0" rel.title = "v1.0.0"
rel.url = "https://github.com/rubenwardy/capturetheflag/archive/master.zip" rel.url = "https://github.com/libregaming/capturetheflag/archive/master.zip"
rel.approved = True rel.approved = True
session.add(rel) session.add(rel)
mod = Package() tool = Package()
mod.state = PackageState.APPROVED tool.state = PackageState.APPROVED
mod.name = "pixelbox" tool.name = "pixelbox"
mod.title = "PixelBOX Reloaded" tool.title = "PixelBOX Reloaded"
mod.license = licenses["CC0"] tool.license = licenses["CC0"]
mod.media_license = licenses["MIT"] tool.media_license = licenses["MIT"]
mod.type = PackageType.TXP # tool.type = PackageType.ASSETPACK
mod.author = admin_user tool.author = admin_user
mod.forums = 14132 tool.forums = 14132
mod.short_desc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit" tool.short_desc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit"
mod.desc = "This is the long desc" tool.desc = "This is the long desc"
session.add(mod) session.add(tool)
rel = PackageRelease() rel = PackageRelease()
rel.package = mod rel.package = tool
rel.title = "v1.0.0" rel.title = "v1.0.0"
rel.url = "http://mamadou3.free.fr/Minetest/PixelBOX.zip" rel.url = "http://mamadou3.free.fr/Minetest/PixelBOX.zip"
rel.approved = True rel.approved = True
@ -378,16 +413,16 @@ Uses the CTF PvP Engine.
session.commit() session.commit()
metas = {} # metas = {}
for package in Package.query.filter_by(type=PackageType.MOD).all(): # for package in Package.query.filter_by(type=PackageType.TOOL).all():
meta = None # meta = None
try: # try:
meta = metas[package.name] # meta = metas[package.name]
except KeyError: # except KeyError:
meta = MetaPackage(package.name) # meta = MetaPackage(package.name)
session.add(meta) # session.add(meta)
metas[package.name] = meta # metas[package.name] = meta
package.provides.append(meta) # package.provides.append(meta)
dep = Dependency(food_sweet, meta=metas["food"]) # dep = Dependency(food_sweet, meta=metas["food"])
session.add(dep) # session.add(dep)

View File

@ -74,7 +74,7 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* PUT `/api/packages/<author>/<name>/` (Update) * PUT `/api/packages/<author>/<name>/` (Update)
* Requires authentication. * Requires authentication.
* JSON dictionary with any of these keys (all are optional, null to delete Nullables): * JSON dictionary with any of these keys (all are optional, null to delete Nullables):
* `type`: One of `GAME`, `MOD`, `TXP`. * `type`: One of `GAME`, `TOOL`, `ASSETPACK`.
* `title`: Human-readable title. * `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved). * `name`: Technical name (needs permission if already approved).
* `short_description` * `short_description`
@ -99,10 +99,10 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* Supports [Package Queries](#package-queries) * Supports [Package Queries](#package-queries)
* [Paginated result](#paginated-results), max 300 results per page * [Paginated result](#paginated-results), max 300 results per page
* Each item in `items` will be a dictionary with the following keys: * Each item in `items` will be a dictionary with the following keys:
* `type`: One of `GAME`, `MOD`, `TXP`. * `type`: One of `GAME`, `TOOL`, `ASSETPACK`.
* `author`: Username of the package author. * `author`: Username of the package author.
* `name`: Package name. * `name`: Package name.
* `provides`: List of technical mod names inside the package. * `provides`: List of technical tool names inside the package.
* `depends`: List of hard dependencies. * `depends`: List of hard dependencies.
* Each dep will either be a metapackage dependency (`name`), or a * Each dep will either be a metapackage dependency (`name`), or a
package dependency (`author/name`). package dependency (`author/name`).
@ -134,11 +134,11 @@ curl -X PUT https://content.minetest.net/api/packages/username/name/ \
Example: Example:
/api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore /api/packages/?type=tool&type=game&q=mobs+fun&hide=nonfree&hide=gore
Supported query parameters: Supported query parameters:
* `type`: Package types (`mod`, `game`, `txp`). * `type`: Package types (`tool`, `game`, `asset_pack`).
* `q`: Query string. * `q`: Query string.
* `author`: Filter by author. * `author`: Filter by author.
* `tag`: Filter by tags. * `tag`: Filter by tags.
@ -173,7 +173,7 @@ Supported query parameters:
* `package` * `package`
* `author`: author username * `author`: author username
* `name`: technical name * `name`: technical name
* `type`: `mod`, `game`, or `txp` * `type`: `tool`, `game`, or `asset_pack`
* GET `/api/packages/<username>/<name>/releases/` (List) * GET `/api/packages/<username>/<name>/releases/` (List)
* Returns array of release dictionaries, see above, but without package info. * Returns array of release dictionaries, see above, but without package info.
* GET `/api/packages/<username>/<name>/releases/<id>/` (Read) * GET `/api/packages/<username>/<name>/releases/<id>/` (Read)
@ -304,7 +304,7 @@ Example:
```json ```json
[ [
{ {
"comment": "This is a really good mod!", "comment": "This is a really good tool!",
"created_at": "2021-11-24T16:18:33.764084", "created_at": "2021-11-24T16:18:33.764084",
"is_positive": true, "is_positive": true,
"title": "Really good", "title": "Really good",
@ -330,12 +330,12 @@ Example:
Example: Example:
/api/topics/?q=mobs&type=mod&type=game /api/topics/?q=mobs&type=tool&type=game
Supported query parameters: Supported query parameters:
* `q`: Query string. * `q`: Query string.
* `type`: Package types (`mod`, `game`, `txp`). * `type`: Package types (`tool`, `game`, `asset_pack`).
* `sort`: Sort by (`name`, `views`, `created_at`). * `sort`: Sort by (`name`, `views`, `created_at`).
* `show_added`: Show topics that have an existing package. * `show_added`: Show topics that have an existing package.
* `show_discarded`: Show topics marked as discarded. * `show_discarded`: Show topics marked as discarded.
@ -390,9 +390,8 @@ Supported query parameters:
* `downloads`: get number of downloads * `downloads`: get number of downloads
* `new`: new packages * `new`: new packages
* `updated`: recently updated packages * `updated`: recently updated packages
* `pop_mod`: popular mods * `popular`: popular packages
* `pop_txp`: popular textures * `toplevel`: toplevel nav tags
* `pop_game`: popular games
* `high_reviewed`: highest reviewed * `high_reviewed`: highest reviewed
* GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning) * GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning)
* `featured`: featured games * `featured`: featured games

View File

@ -13,7 +13,7 @@ If you don't, then you can just sign up using an email address and password.
GitHub can only be used to login, not to register. GitHub can only be used to login, not to register.
<a class="btn btn-primary" href="/user/claim/">Register</a> <a class="btn btn-primary" href="/user/register/">Register</a>
### My verification email never arrived ### My verification email never arrived

View File

@ -69,8 +69,8 @@ is available.
* MUST: `screenshot.png` is present and up-to-date, with a correct aspect ratio (3:2, at least 300x200). * MUST: `screenshot.png` is present and up-to-date, with a correct aspect ratio (3:2, at least 300x200).
* MUST: Have a high resolution cover image on ContentDB (at least 1280x720 pixels). * MUST: Have a high resolution cover image on ContentDB (at least 1280x720 pixels).
It may be shown cropped to 16:9 aspect ratio, or shorter. It may be shown cropped to 16:9 aspect ratio, or shorter.
* MUST: mod.conf/game.conf/texture_pack.conf present with: * MUST: tool.conf/game.conf/texture_pack.conf present with:
* name (if mod or game) * name (if tool or game)
* description * description
* dependencies (if relevant) * dependencies (if relevant)
* `min_minetest_version` and `max_minetest_version` (if relevant) * `min_minetest_version` and `max_minetest_version` (if relevant)

View File

@ -14,7 +14,7 @@ and they will be subject to limited promotion.
of packages with non-free licenses.** of packages with non-free licenses.**
Minetest is free and open source software, and is only as big as it is now Minetest is free and open source software, and is only as big as it is now
because of this. It's pretty amazing you can take nearly any published mod and modify it because of this. It's pretty amazing you can take nearly any published tool and modify it
to how you like - add some features, maybe fix some bugs - and then share those to how you like - add some features, maybe fix some bugs - and then share those
modifications without the worry of legal issues. The project, itself, relies on open modifications without the worry of legal issues. The project, itself, relies on open
source contributions to survive - if it were non-free, then it would have died source contributions to survive - if it were non-free, then it would have died

View File

@ -14,8 +14,8 @@ Every type of content can have a `.conf` file that contains the metadata.
The filename of the `.conf` file depends on the content type: The filename of the `.conf` file depends on the content type:
* `mod.conf` for mods. * `tool.conf` for mods.
* `modpack.conf` for mod packs. * `modpack.conf` for tool packs.
* `game.conf` for games. * `game.conf` for games.
* `texture_pack.conf` for texture packs. * `texture_pack.conf` for texture packs.
@ -36,7 +36,7 @@ ContentDB understands the following information:
and for mods only: and for mods only:
* `name` - the mod technical name. * `name` - the tool technical name.
## .cdb.json ## .cdb.json
@ -46,7 +46,7 @@ to update the package meta.
It should be a JSON dictionary with one or more of the following optional keys: It should be a JSON dictionary with one or more of the following optional keys:
* `type`: One of `GAME`, `MOD`, `TXP`. * `type`: One of `GAME`, `TOOL`, `ASSETPACK`.
* `title`: Human-readable title. * `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved). * `name`: Technical name (needs permission if already approved).
* `short_description` * `short_description`

View File

@ -3,7 +3,7 @@ title: Top Packages Algorithm
## Package Score ## Package Score
Each package is given a `score`, which is used when ordering them in the Each package is given a `score`, which is used when ordering them in the
"Top Games/Mods/Texture Packs" lists. The intention of this feature is "Top Games/Tools/Asset Packs" lists. The intention of this feature is
to make it easier for new users to find good packages. to make it easier for new users to find good packages.
A package's score is equal to a rolling average of recent downloads, A package's score is equal to a rolling average of recent downloads,

View File

@ -8,7 +8,7 @@ the listings and to combat abuse.
* **No inappropriate content.** <sup>2.1</sup> * **No inappropriate content.** <sup>2.1</sup>
* **Content must be playable/useful, but not necessarily finished.** <sup>2.2</sup> * **Content must be playable/useful, but not necessarily finished.** <sup>2.2</sup>
* **Don't use the name of another mod unless your mod is a fork or reimplementation.** <sup>3</sup> * **Don't use the name of another tool unless your tool is a fork or reimplementation.** <sup>3</sup>
* **Licenses must allow derivatives, redistribution, and must not discriminate.** <sup>4</sup> * **Licenses must allow derivatives, redistribution, and must not discriminate.** <sup>4</sup>
* **Don't put promotions or advertisements in any package metadata.** <sup>5</sup> * **Don't put promotions or advertisements in any package metadata.** <sup>5</sup>
* **The ContentDB admin reserves the right to remove packages for any reason**, * **The ContentDB admin reserves the right to remove packages for any reason**,
@ -51,14 +51,14 @@ as this will help advise players.
Adding non-player facing mods, such as libraries and server tools, is perfectly fine Adding non-player facing mods, such as libraries and server tools, is perfectly fine
and encouraged. ContentDB isn't just for player-facing things, and adding and encouraged. ContentDB isn't just for player-facing things, and adding
libraries allows them to be installed when a mod depends on it. libraries allows them to be installed when a tool depends on it.
## 3. Technical Names ## 3. Technical Names
### 3.1 Right to a name ### 3.1 Right to a name
A package uses a name when it has that name or contains a mod that uses that name. A package uses a name when it has that name or contains a tool that uses that name.
The first package to use a name based on the creation of its forum topic or The first package to use a name based on the creation of its forum topic or
ContentDB submission has the right to the technical name. The use of a package ContentDB submission has the right to the technical name. The use of a package
@ -74,14 +74,14 @@ to change the name of the package, or your package won't be accepted.
We reserve the right to issue exceptions for this where we feel necessary. We reserve the right to issue exceptions for this where we feel necessary.
### 3.2. Mod Forks and Reimplementations ### 3.2. Tool Forks and Reimplementations
An exception to the above is that mods are allowed to have the same name as a An exception to the above is that mods are allowed to have the same name as a
mod if it's a fork of that mod (or a close reimplementation). In real terms, it tool if it's a fork of that tool (or a close reimplementation). In real terms, it
should be possible to use the new mod as a drop-in replacement. should be possible to use the new tool as a drop-in replacement.
We reserve the right to decide whether a mod counts as a fork or We reserve the right to decide whether a tool counts as a fork or
reimplementation of the mod that owns the name. reimplementation of the tool that owns the name.
## 4. Licenses ## 4. Licenses

View File

@ -83,9 +83,9 @@ Please [raise a report](https://content.minetest.net/report/?anon=0) if you
wish to remove your personal information. wish to remove your personal information.
ContentDB keeps a record of each username and forum topic on the forums, ContentDB keeps a record of each username and forum topic on the forums,
for use in indexing mod/game topics. ContentDB also requires the use of a username for use in indexing tool/game topics. ContentDB also requires the use of a username
to uniquely identify a package. Therefore, an author cannot be removed completely to uniquely identify a package. Therefore, an author cannot be removed completely
from ContentDB if they have any packages or mod/game topics on the forum. from ContentDB if they have any packages or tool/game topics on the forum.
If we are unable to remove your account for one of the above reasons, your user If we are unable to remove your account for one of the above reasons, your user
account will instead be wiped and deactivated, ending up exactly like an author account will instead be wiped and deactivated, ending up exactly like an author

View File

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

View File

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

View File

@ -22,7 +22,7 @@ from flask_babel import lazy_gettext
from app.logic.LogicError import LogicError from app.logic.LogicError import LogicError
from app.logic.uploads import upload_file from app.logic.uploads import upload_file
from app.models import PackageRelease, db, Permission, User, Package, MinetestRelease from app.models import PackageRelease, db, Permission, User, Package
from app.tasks.importtasks import makeVCSRelease, checkZipRelease from app.tasks.importtasks import makeVCSRelease, checkZipRelease
from app.utils import AuditSeverity, addAuditLog, nonEmptyOrNone from app.utils import AuditSeverity, addAuditLog, nonEmptyOrNone
@ -38,7 +38,7 @@ def check_can_create_release(user: User, package: Package):
def do_create_vcs_release(user: User, package: Package, title: str, ref: str, def do_create_vcs_release(user: User, package: Package, title: str, ref: str,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None): min_v = None, max_v = None, reason: str = None):
check_can_create_release(user, package) check_can_create_release(user, package)
rel = PackageRelease() rel = PackageRelease()
@ -46,8 +46,6 @@ def do_create_vcs_release(user: User, package: Package, title: str, ref: str,
rel.title = title rel.title = title
rel.url = "" rel.url = ""
rel.task_id = uuid() rel.task_id = uuid()
rel.min_rel = min_v
rel.max_rel = max_v
db.session.add(rel) db.session.add(rel)
if reason is None: if reason is None:
@ -64,7 +62,7 @@ def do_create_vcs_release(user: User, package: Package, title: str, ref: str,
def do_create_zip_release(user: User, package: Package, title: str, file, def do_create_zip_release(user: User, package: Package, title: str, file,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None, min_v = None, max_v = None, reason: str = None,
commit_hash: str = None): commit_hash: str = None):
check_can_create_release(user, package) check_can_create_release(user, package)
@ -81,8 +79,7 @@ def do_create_zip_release(user: User, package: Package, title: str, file,
rel.url = uploaded_url rel.url = uploaded_url
rel.task_id = uuid() rel.task_id = uuid()
rel.commit_hash = commit_hash rel.commit_hash = commit_hash
rel.min_rel = min_v
rel.max_rel = max_v
db.session.add(rel) db.session.add(rel)
if reason is None: if reason is None:

View File

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

View File

@ -23,7 +23,7 @@ from flask_babel import lazy_gettext
from flask_sqlalchemy import BaseQuery from flask_sqlalchemy import BaseQuery
from sqlalchemy_searchable import SearchQueryMixin from sqlalchemy_searchable import SearchQueryMixin
from sqlalchemy_utils.types import TSVectorType from sqlalchemy_utils.types import TSVectorType
from app.markdown import render_markdown
from . import db from . import db
from .users import Permission, UserRank, User from .users import Permission, UserRank, User
from .. import app from .. import app
@ -48,49 +48,49 @@ class License(db.Model):
return self.name return self.name
class PackageType(enum.Enum): # class PackageType(enum.Enum):
MOD = "Mod" # GAME = "Game"
GAME = "Game" # TOOL = "Tool"
TXP = "Texture Pack" # ASSETPACK = "Asset Pack"
def toName(self): # def toName(self):
return self.name.lower() # return self.name.lower()
def __str__(self): # def __str__(self):
return self.name # return self.name
@property # @property
def text(self): # def text(self):
if self == PackageType.MOD: # if self == PackageType.TOOL:
return lazy_gettext("Mod") # return lazy_gettext("Tool")
elif self == PackageType.GAME: # elif self == PackageType.GAME:
return lazy_gettext("Game") # return lazy_gettext("Game")
elif self == PackageType.TXP: # elif self == PackageType.ASSETPACK:
return lazy_gettext("Texture Pack") # return lazy_gettext("Asset Pack")
@property # @property
def plural(self): # def plural(self):
if self == PackageType.MOD: # if self == PackageType.TOOL:
return lazy_gettext("Mods") # return lazy_gettext("Tools")
elif self == PackageType.GAME: # elif self == PackageType.GAME:
return lazy_gettext("Games") # return lazy_gettext("Games")
elif self == PackageType.TXP: # elif self == PackageType.ASSETPACK:
return lazy_gettext("Texture Packs") # return lazy_gettext("Asset Packs")
@classmethod # @classmethod
def get(cls, name): # def get(cls, name):
try: # try:
return PackageType[name.upper()] # return PackageType[name.upper()]
except KeyError: # except KeyError:
return None # return None
@classmethod # @classmethod
def choices(cls): # def choices(cls):
return [(choice, choice.text) for choice in cls] # return [(choice, choice.text) for choice in cls]
@classmethod # @classmethod
def coerce(cls, item): # def coerce(cls, item):
return item if type(item) == PackageType else PackageType[item.upper()] # return item if type(item) == PackageType else PackageType[item.upper()]
class PackageDevState(enum.Enum): class PackageDevState(enum.Enum):
@ -218,7 +218,7 @@ class PackagePropertyKey(enum.Enum):
title = "Title" title = "Title"
short_desc = "Short Description" short_desc = "Short Description"
desc = "Description" desc = "Description"
type = "Type" # type = "Type"
license = "License" license = "License"
media_license = "Media License" media_license = "Media License"
tags = "Tags" tags = "Tags"
@ -226,7 +226,7 @@ class PackagePropertyKey(enum.Enum):
repo = "Repository" repo = "Repository"
website = "Website" website = "Website"
issueTracker = "Issue Tracker" issueTracker = "Issue Tracker"
forums = "Forum Topic ID" # forums = "Forum Topic ID"
def convert(self, value): def convert(self, value):
if self == PackagePropertyKey.tags: if self == PackagePropertyKey.tags:
@ -376,11 +376,13 @@ class Package(db.Model):
title = db.Column(db.Unicode(100), nullable=False) title = db.Column(db.Unicode(100), nullable=False)
short_desc = db.Column(db.Unicode(200), nullable=False) short_desc = db.Column(db.Unicode(200), nullable=False)
desc = db.Column(db.UnicodeText, nullable=True) desc = db.Column(db.UnicodeText, nullable=True)
type = db.Column(db.Enum(PackageType), nullable=False) build_desc = db.Column(db.UnicodeText, nullable=True)
install_desc = db.Column(db.UnicodeText, nullable=True)
# type = db.Column(db.Enum(PackageType), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
approved_at = db.Column(db.DateTime, nullable=True, default=None) approved_at = db.Column(db.DateTime, nullable=True, default=None)
name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'") name_valid = db.CheckConstraint("name ~* '^[a-zA-Z0-9_\-\.]+$'")
search_vector = db.Column(TSVectorType("name", "title", "short_desc", "desc", search_vector = db.Column(TSVectorType("name", "title", "short_desc", "desc",
weights={ "name": "A", "title": "B", "short_desc": "C", "desc": "D" })) weights={ "name": "A", "title": "B", "short_desc": "C", "desc": "D" }))
@ -532,7 +534,7 @@ class Package(db.Model):
"title": self.title, "title": self.title,
"author": self.author.username, "author": self.author.username,
"short_description": short_desc, "short_description": short_desc,
"type": self.type.toName(), # "type": self.type.toName(),
"release": release_id, "release": release_id,
"thumbnail": (base_url + tnurl) if tnurl is not None else None, "thumbnail": (base_url + tnurl) if tnurl is not None else None,
"aliases": [ alias.getAsDictionary() for alias in self.aliases ], "aliases": [ alias.getAsDictionary() for alias in self.aliases ],
@ -557,7 +559,7 @@ class Package(db.Model):
"title": self.title, "title": self.title,
"short_description": self.short_desc, "short_description": self.short_desc,
"long_description": self.desc, "long_description": self.desc,
"type": self.type.toName(), # "type": self.type.toName(),
"created_at": self.created_at.isoformat(), "created_at": self.created_at.isoformat(),
"license": self.license.name, "license": self.license.name,
@ -636,9 +638,7 @@ class Package(db.Model):
def getDownloadRelease(self, version=None): def getDownloadRelease(self, version=None):
for rel in self.releases: for rel in self.releases:
if rel.approved and (version is None or if rel.approved:
((rel.min_rel is None or rel.min_rel_id <= version.id) and
(rel.max_rel is None or rel.max_rel_id >= version.id))):
return rel return rel
return None return None
@ -728,7 +728,7 @@ class Package(db.Model):
return False return False
needsScreenshot = \ needsScreenshot = \
(self.type == self.type.GAME or self.type == self.type.TXP) and \ (self.type == self.type.GAME or self.type == self.type.ASSETPACK) and \
self.screenshots.count() == 0 self.screenshots.count() == 0
return self.releases.filter(PackageRelease.task_id.is_(None)).count() > 0 and not needsScreenshot return self.releases.filter(PackageRelease.task_id.is_(None)).count() > 0 and not needsScreenshot
@ -765,14 +765,13 @@ class Package(db.Model):
review_scores = [ 100 * r.asSign() for r in self.reviews ] review_scores = [ 100 * r.asSign() for r in self.reviews ]
self.score = self.score_downloads + sum(review_scores) self.score = self.score_downloads + sum(review_scores)
class MetaPackage(db.Model): class MetaPackage(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False) name = db.Column(db.String(100), unique=True, nullable=False)
dependencies = db.relationship("Dependency", back_populates="meta_package", lazy="dynamic") dependencies = db.relationship("Dependency", back_populates="meta_package", lazy="dynamic")
packages = db.relationship("Package", lazy="dynamic", back_populates="provides", secondary=PackageProvides) packages = db.relationship("Package", lazy="dynamic", back_populates="provides", secondary=PackageProvides)
mp_name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'") mp_name_valid = db.CheckConstraint("name ~* '^[a-zA-Z0-9_\-\.]+$'")
def __init__(self, name=None): def __init__(self, name=None):
self.name = name self.name = name
@ -848,6 +847,7 @@ class Tag(db.Model):
textColor = db.Column(db.String(6), nullable=False) textColor = db.Column(db.String(6), nullable=False)
views = db.Column(db.Integer, nullable=False, default=0) views = db.Column(db.Integer, nullable=False, default=0)
is_protected = db.Column(db.Boolean, nullable=False, default=False) is_protected = db.Column(db.Boolean, nullable=False, default=False)
is_toplevel = db.Column(db.Boolean, nullable=False, default=False)
packages = db.relationship("Package", back_populates="tags", secondary=Tags) packages = db.relationship("Package", back_populates="tags", secondary=Tags)
@ -857,7 +857,7 @@ class Tag(db.Model):
self.textColor = textColor self.textColor = textColor
import re import re
regex = re.compile("[^a-z_]") regex = re.compile("[^0-9a-z_]")
self.name = regex.sub("", self.title.lower().replace(" ", "_")) self.name = regex.sub("", self.title.lower().replace(" ", "_"))
def getAsDictionary(self): def getAsDictionary(self):
@ -867,52 +867,11 @@ class Tag(db.Model):
"title": self.title, "title": self.title,
"description": description, "description": description,
"is_protected": self.is_protected, "is_protected": self.is_protected,
"is_toplevel": self.is_toplevel,
"views": self.views, "views": self.views,
} }
class MinetestRelease(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
protocol = db.Column(db.Integer, nullable=False, default=0)
def __init__(self, name=None, protocol=0):
self.name = name
self.protocol = protocol
def getActual(self):
return None if self.name == "None" else self
def getAsDictionary(self):
return {
"name": self.name,
"protocol_version": self.protocol,
"is_dev": "-dev" in self.name,
}
@classmethod
def get(cls, version, protocol_num):
if version:
parts = version.strip().split(".")
if len(parts) >= 2:
major_minor = parts[0] + "." + parts[1]
query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor)))
if protocol_num:
query = query.filter_by(protocol=protocol_num)
release = query.one_or_none()
if release:
return release
if protocol_num:
# Find the closest matching release
return MinetestRelease.query.order_by(db.desc(MinetestRelease.protocol),
db.desc(MinetestRelease.id)) \
.filter(MinetestRelease.protocol <= protocol_num).first()
return None
class PackageRelease(db.Model): class PackageRelease(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -927,11 +886,7 @@ class PackageRelease(db.Model):
commit_hash = db.Column(db.String(41), nullable=True, default=None) commit_hash = db.Column(db.String(41), nullable=True, default=None)
downloads = db.Column(db.Integer, nullable=False, default=0) downloads = db.Column(db.Integer, nullable=False, default=0)
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None) channel = db.Column(db.String(200), nullable=False, default="")
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
# If the release is approved, then the task_id must be null and the url must be present # If the release is approved, then the task_id must be null and the url must be present
CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)") CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
@ -948,8 +903,7 @@ class PackageRelease(db.Model):
"release_date": self.releaseDate.isoformat(), "release_date": self.releaseDate.isoformat(),
"commit": self.commit_hash, "commit": self.commit_hash,
"downloads": self.downloads, "downloads": self.downloads,
"min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(), "channel": self.channel,
"max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(),
} }
def getLongAsDictionary(self): def getLongAsDictionary(self):
@ -960,8 +914,7 @@ class PackageRelease(db.Model):
"release_date": self.releaseDate.isoformat(), "release_date": self.releaseDate.isoformat(),
"commit": self.commit_hash, "commit": self.commit_hash,
"downloads": self.downloads, "downloads": self.downloads,
"min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(), "channel": self.channel,
"max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(),
"package": self.package.getAsDictionaryKey() "package": self.package.getAsDictionaryKey()
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

View File

@ -53,7 +53,7 @@ ni:" ac co com edu gob mil net nom org ",np:" com edu gov mil net org ",nr:" biz
ps:" com edu gov net org plo sec ",pw:" belau co ed go ne or ",ro:" arts com firm info nom nt org rec store tm www ",rs:" ac co edu gov in org ",sb:" com edu gov net org ",sc:" com edu gov net org ",sh:" co com edu gov net nom org ",sl:" com edu gov net org ",st:" co com consulado edu embaixada gov mil net org principe saotome store ",sv:" com edu gob org red ",sz:" ac co org ",tr:" av bbs bel biz com dr edu gen gov info k12 name net org pol tel tsk tv web ",tt:" aero biz cat co com coop edu gov info int jobs mil mobi museum name net org pro tel travel ", ps:" com edu gov net org plo sec ",pw:" belau co ed go ne or ",ro:" arts com firm info nom nt org rec store tm www ",rs:" ac co edu gov in org ",sb:" com edu gov net org ",sc:" com edu gov net org ",sh:" co com edu gov net nom org ",sl:" com edu gov net org ",st:" co com consulado edu embaixada gov mil net org principe saotome store ",sv:" com edu gob org red ",sz:" ac co org ",tr:" av bbs bel biz com dr edu gen gov info k12 name net org pol tel tsk tv web ",tt:" aero biz cat co com coop edu gov info int jobs mil mobi museum name net org pro tel travel ",
tw:" club com ebiz edu game gov idv mil net org ",mu:" ac co com gov net or org ",mz:" ac co edu gov org ",na:" co com ",nz:" ac co cri geek gen govt health iwi maori mil net org parliament school ",pa:" abo ac com edu gob ing med net nom org sld ",pt:" com edu gov int net nome org publ ",py:" com edu gov mil net org ",qa:" com edu gov mil net org ",re:" asso com nom ",ru:" ac adygeya altai amur arkhangelsk astrakhan bashkiria belgorod bir bryansk buryatia cbg chel chelyabinsk chita chukotka chuvashia com dagestan e-burg edu gov grozny int irkutsk ivanovo izhevsk jar joshkar-ola kalmykia kaluga kamchatka karelia kazan kchr kemerovo khabarovsk khakassia khv kirov koenig komi kostroma kranoyarsk kuban kurgan kursk lipetsk magadan mari mari-el marine mil mordovia mosreg msk murmansk nalchik net nnov nov novosibirsk nsk omsk orenburg org oryol penza perm pp pskov ptz rnd ryazan sakhalin samara saratov simbirsk smolensk spb stavropol stv surgut tambov tatarstan tom tomsk tsaritsyn tsk tula tuva tver tyumen udm udmurtia ulan-ude vladikavkaz vladimir vladivostok volgograd vologda voronezh vrn vyatka yakutia yamal yekaterinburg yuzhno-sakhalinsk ", tw:" club com ebiz edu game gov idv mil net org ",mu:" ac co com gov net or org ",mz:" ac co edu gov org ",na:" co com ",nz:" ac co cri geek gen govt health iwi maori mil net org parliament school ",pa:" abo ac com edu gob ing med net nom org sld ",pt:" com edu gov int net nome org publ ",py:" com edu gov mil net org ",qa:" com edu gov mil net org ",re:" asso com nom ",ru:" ac adygeya altai amur arkhangelsk astrakhan bashkiria belgorod bir bryansk buryatia cbg chel chelyabinsk chita chukotka chuvashia com dagestan e-burg edu gov grozny int irkutsk ivanovo izhevsk jar joshkar-ola kalmykia kaluga kamchatka karelia kazan kchr kemerovo khabarovsk khakassia khv kirov koenig komi kostroma kranoyarsk kuban kurgan kursk lipetsk magadan mari mari-el marine mil mordovia mosreg msk murmansk nalchik net nnov nov novosibirsk nsk omsk orenburg org oryol penza perm pp pskov ptz rnd ryazan sakhalin samara saratov simbirsk smolensk spb stavropol stv surgut tambov tatarstan tom tomsk tsaritsyn tsk tula tuva tver tyumen udm udmurtia ulan-ude vladikavkaz vladimir vladivostok volgograd vologda voronezh vrn vyatka yakutia yamal yekaterinburg yuzhno-sakhalinsk ",
rw:" ac co com edu gouv gov int mil net ",sa:" com edu gov med net org pub sch ",sd:" com edu gov info med net org tv ",se:" a ac b bd c d e f g h i k l m n o org p parti pp press r s t tm u w x y z ",sg:" com edu gov idn net org per ",sn:" art com edu gouv org perso univ ",sy:" com edu gov mil net news org ",th:" ac co go in mi net or ",tj:" ac biz co com edu go gov info int mil name net nic org test web ",tn:" agrinet com defense edunet ens fin gov ind info intl mincom nat net org perso rnrt rns rnu tourism ", rw:" ac co com edu gouv gov int mil net ",sa:" com edu gov med net org pub sch ",sd:" com edu gov info med net org tv ",se:" a ac b bd c d e f g h i k l m n o org p parti pp press r s t tm u w x y z ",sg:" com edu gov idn net org per ",sn:" art com edu gouv org perso univ ",sy:" com edu gov mil net news org ",th:" ac co go in mi net or ",tj:" ac biz co com edu go gov info int mil name net nic org test web ",tn:" agrinet com defense edunet ens fin gov ind info intl mincom nat net org perso rnrt rns rnu tourism ",
tz:" ac co go ne or ",ua:" biz cherkassy chernigov chernovtsy ck cn co com crimea cv dn dnepropetrovsk donetsk dp edu gov if in ivano-frankivsk kh kharkov kherson khmelnitskiy kiev kirovograd km kr ks kv lg lugansk lutsk lviv me mk net nikolaev od odessa org pl poltava pp rovno rv sebastopol sumy te ternopil uzhgorod vinnica vn zaporizhzhe zhitomir zp zt ",ug:" ac co go ne or org sc ",uk:" ac bl british-library co cym gov govt icnet jet lea ltd me mil mod national-library-scotland nel net nhs nic nls org orgn parliament plc police sch scot soc ", tz:" ac co go ne or ",ua:" biz cherkassy chernigov chernovtsy ck cn co com crimea cv dn dnepropetrovsk donetsk dp edu gov if in ivano-frankivsk kh kharkov kherson khmelnitskiy kiev kirovograd km kr ks kv lg lugansk lutsk lviv me mk net nikolaev od odessa org pl poltava pp rovno rv sebastopol sumy te ternopil uzhgorod vinnica vn zaporizhzhe zhitomir zp zt ",ug:" ac co go ne or org sc ",uk:" ac bl british-library co cym gov govt icnet jet lea ltd me mil tool national-library-scotland nel net nhs nic nls org orgn parliament plc police sch scot soc ",
us:" dni fed isa kids nsn ",uy:" com edu gub mil net org ",ve:" co com edu gob info mil net org web ",vi:" co com k12 net org ",vn:" ac biz com edu gov health info int name net org pro ",ye:" co com gov ltd me net org plc ",yu:" ac co edu gov org ",za:" ac agric alt bourse city co cybernet db edu gov grondar iaccess imt inca landesign law mil net ngo nis nom olivetti org pix school tm web ",zm:" ac co com edu gov net org sch ",com:"ar br cn de eu gb gr hu jpn kr no qc ru sa se uk us uy za ",net:"gb jp se uk ", us:" dni fed isa kids nsn ",uy:" com edu gub mil net org ",ve:" co com edu gob info mil net org web ",vi:" co com k12 net org ",vn:" ac biz com edu gov health info int name net org pro ",ye:" co com gov ltd me net org plc ",yu:" ac co edu gov org ",za:" ac agric alt bourse city co cybernet db edu gov grondar iaccess imt inca landesign law mil net ngo nis nom olivetti org pix school tm web ",zm:" ac co com edu gov net org sch ",com:"ar br cn de eu gb gr hu jpn kr no qc ru sa se uk us uy za ",net:"gb jp se uk ",
org:"ae",de:"com "},has:function(k){var d=k.lastIndexOf(".");if(0>=d||d>=k.length-1)return!1;var m=k.lastIndexOf(".",d-1);if(0>=m||m>=d-1)return!1;var x=n.list[k.slice(d+1)];return x?0<=x.indexOf(" "+k.slice(m+1,d)+" "):!1},is:function(k){var d=k.lastIndexOf(".");if(0>=d||d>=k.length-1||0<=k.lastIndexOf(".",d-1))return!1;var m=n.list[k.slice(d+1)];return m?0<=m.indexOf(" "+k.slice(0,d)+" "):!1},get:function(k){var d=k.lastIndexOf(".");if(0>=d||d>=k.length-1)return null;var m=k.lastIndexOf(".",d-1); org:"ae",de:"com "},has:function(k){var d=k.lastIndexOf(".");if(0>=d||d>=k.length-1)return!1;var m=k.lastIndexOf(".",d-1);if(0>=m||m>=d-1)return!1;var x=n.list[k.slice(d+1)];return x?0<=x.indexOf(" "+k.slice(m+1,d)+" "):!1},is:function(k){var d=k.lastIndexOf(".");if(0>=d||d>=k.length-1||0<=k.lastIndexOf(".",d-1))return!1;var m=n.list[k.slice(d+1)];return m?0<=m.indexOf(" "+k.slice(0,d)+" "):!1},get:function(k){var d=k.lastIndexOf(".");if(0>=d||d>=k.length-1)return null;var m=k.lastIndexOf(".",d-1);
if(0>=m||m>=d-1)return null;var x=n.list[k.slice(d+1)];return!x||0>x.indexOf(" "+k.slice(m+1,d)+" ")?null:k.slice(m+1)},noConflict:function(){t.SecondLevelDomains===this&&(t.SecondLevelDomains=w);return this}};return n}); if(0>=m||m>=d-1)return null;var x=n.list[k.slice(d+1)];return!x||0>x.indexOf(" "+k.slice(m+1,d)+" ")?null:k.slice(m+1)},noConflict:function(){t.SecondLevelDomains===this&&(t.SecondLevelDomains=w);return this}};return n});

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>ContentDB</ShortName> <ShortName>Okapi</ShortName>
<LongName>ContentDB</LongName> <LongName>Okapi ContentDB</LongName>
<InputEncoding>UTF-8</InputEncoding> <InputEncoding>UTF-8</InputEncoding>
<Description>Search mods, games, and textures for Minetest.</Description> <Description>Search libre games, tools and mods</Description>
<Tags>Minetest Mod Game Subgame Search</Tags> <Tags>Libre Tool Game Subgame Search</Tags>
<Url type="text/html" method="get" template="https://content.minetest.net/packages?q={searchTerms}"/> <Url type="text/html" method="get" template="https://games.armen138.com/packages?q={searchTerms}"/>
</OpenSearchDescription> </OpenSearchDescription>

View File

@ -38,7 +38,7 @@ $(function() {
} }
let hint_mtmods = `Tip: let hint_mtmods = `Tip:
Don't include <i>Minetest</i>, <i>mod</i>, or <i>modpack</i> anywhere in the short description. Don't include <i>Minetest</i>, <i>tool</i>, or <i>modpack</i> anywhere in the short description.
It is unnecessary and wastes characters.`; It is unnecessary and wastes characters.`;
let hint_thegame = `Tip: let hint_thegame = `Tip:
@ -47,8 +47,8 @@ $(function() {
$("#short_desc").on("change paste keyup", function() { $("#short_desc").on("change paste keyup", function() {
const val = $(this).val().toLowerCase(); const val = $(this).val().toLowerCase();
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 || if (val.indexOf("minetest") >= 0 || val.indexOf("tool") >= 0 ||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) { val.indexOf("modpack") >= 0 || val.indexOf("tool pack") >= 0) {
showHint($(this), hint_mtmods); showHint($(this), hint_mtmods);
} else if (val.indexOf("the game") >= 0) { } else if (val.indexOf("the game") >= 0) {
showHint($(this), hint_thegame); showHint($(this), hint_thegame);

View File

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

View File

@ -66,10 +66,10 @@ def make_celery(app):
celery = make_celery(app) celery = make_celery(app)
CELERYBEAT_SCHEDULE = { CELERYBEAT_SCHEDULE = {
'topic_list_import': { # 'topic_list_import': {
'task': 'app.tasks.forumtasks.importTopicList', # 'task': 'app.tasks.forumtasks.importTopicList',
'schedule': crontab(minute=1, hour=1), # 0101 # 'schedule': crontab(minute=1, hour=1), # 0101
}, # },
'package_score_update': { 'package_score_update': {
'task': 'app.tasks.pkgtasks.updatePackageScores', 'task': 'app.tasks.pkgtasks.updatePackageScores',
'schedule': crontab(minute=10, hour=1), # 0110 'schedule': crontab(minute=10, hour=1), # 0110
@ -78,10 +78,10 @@ CELERYBEAT_SCHEDULE = {
'task': 'app.tasks.importtasks.check_for_updates', 'task': 'app.tasks.importtasks.check_for_updates',
'schedule': crontab(minute=10, hour=1), # 0110 'schedule': crontab(minute=10, hour=1), # 0110
}, },
'send_pending_notifications': { # 'send_pending_notifications': {
'task': 'app.tasks.emails.send_pending_notifications', # 'task': 'app.tasks.emails.send_pending_notifications',
'schedule': crontab(minute='*/5'), # every 5 minutes # 'schedule': crontab(minute='*/5'), # every 5 minutes
}, # },
'send_notification_digests': { 'send_notification_digests': {
'task': 'app.tasks.emails.send_pending_digests', 'task': 'app.tasks.emails.send_pending_digests',
'schedule': crontab(minute=0, hour=14), # 1400 'schedule': crontab(minute=0, hour=14), # 1400

205
app/tasks/appstreamtasks.py Normal file
View File

@ -0,0 +1,205 @@
import json, re, sys
from app.models import *
from app.tasks import celery
from app.utils import is_username_valid
import urllib.request
import gi
import PIL
import requests
import os
import sys
import inspect
import shutil
import urllib.request
from sqlalchemy.sql import func
from gi.repository import Gio
gi.require_version('AppStreamGlib', '1.0')
from gi.repository import AppStreamGlib
from app.utils.lists import alwaysAccept, alwaysDeny, badLicenses, badCategories, nonFreeAssets, nonFreeNetworkServices
import itertools
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)]
urls = [(app.get_url_item(kind),kind.value_nick) for kind in kinds]
return list(filter(lambda a: a[0] is not None, urls))
def acceptedGame(app):
#return 'Game' in app.get_categories()
if app.get_id() in alwaysAccept:
return True
if app.get_id() in alwaysDeny:
return False
return app.get_project_license() and \
not [x for x in badLicenses if x in app.get_project_license()] and \
'Game' in app.get_categories() and \
not [x for x in badCategories if x in app.get_categories()]
def getScreenshots(app):
return [images.get_source() for images in app.get_screenshots()]
@celery.task()
def importFromFlathub():
url = "https://flathub.org/repo/appstream/x86_64/appstream.xml.gz"
with urllib.request.urlopen(url) as response, open("/var/cdb/uploads/appstream.xml.gz", 'wb') as out_file:
shutil.copyfileobj(response, out_file)
store = AppStreamGlib.Store()
file = Gio.File.new_for_path("/var/cdb/uploads/appstream.xml.gz")
file.load_contents()
AppStreamGlib.Store.from_file(store, file, ".", None)
apps = list(filter(acceptedGame, store.get_apps()))
session=db.session
licenses = { x.name : x for x in License.query.all() }
tags = { x.name : x for x in Tag.query.all() }
admin_user = User.query.filter_by(username="AppStreamBot").first()
if not admin_user:
admin_user = User("AppStreamBot")
admin_user.is_active = True
admin_user.password = make_flask_login_password("AppStreamBot")
admin_user.github_username = "AppStreamBot"
admin_user.forums_username = "AppStreamBot"
admin_user.rank = UserRank.ADMIN
session.add(admin_user)
featured = Tag.query.filter_by(name="featured").first()
for app in apps:
screenshots = getScreenshots(app)
urls = get_urls(app)
filename = app.get_name().replace(':', '').replace('/','') + ".html"
print("APPLICATION: ",app.get_name())
package_exists = Package.query.filter_by(name=app.get_id()).first()
if package_exists:
print(f"Package {app.get_id()} exists, skipping.")
else:
game1 = Package()
game1.state = PackageState.APPROVED
game1.name = app.get_id()
game1.title = app.get_name()
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()
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:
game1.tags.append(featured)
game1.license = licenses[license]
game1.media_license = licenses["MIT"]
game1.author = admin_user
for url,t in urls:
if t == "bugtracker":
game1.issueTracker = url
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() + "\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"
game1.install_desc += f"\n```\nflatpak run {app.get_id()}\n```\n"
session.add(game1)
install_url = f"https://dl.flathub.org/repo/appstream/{app.get_id()}.flatpakref"
release = PackageRelease()
release.package = game1
release.title = "Flathub Install"
release.url = install_url
release.approved = True
release.downloads = 0
release.releaseDate = func.now()
session.add(release)
for screenshot in screenshots:
counter = 1
url = screenshot.get_url()
try:
r = requests.get(url,timeout=10)
r.raise_for_status()
filename = randomString(10) + "." + "png"
filepath = os.path.join("/var/cdb/uploads", filename)
print("Screenshot url: ", url)
with open(filepath,"wb") as f:
f.write(r.content)
width, height = get_image_size(filepath)
if (width is not None) and (height is not None):
ss = PackageScreenshot()
ss.package = game1
ss.title = "Untitled"
ss.url = "/uploads/" + filename
ss.width = width
ss.height = height
ss.approved = True
ss.order = counter
session.add(ss)
session.commit()
game1.cover_image = ss
session.commit()
counter += 1
except requests.exceptions.HTTPError as err:
print("HTTP error downloading the screenshot ", err)
except requests.exceptions.ConnectionError as err:
print("HTTP error downloading the screenshot ", err)
except requests.exceptions.ReadTimeout as err:
print("Screenshot timeout ", err)
except PIL.UnidentifiedImageError as err:
print("Corrupt image ", err)
session.commit()
def importAppstream():
pass

View File

@ -80,7 +80,7 @@ def checkAllForumAccounts(forceNoSave=False):
regex_tag = re.compile(r"\[([a-z0-9_]+)\]") regex_tag = re.compile(r"\[([a-z0-9_]+)\]")
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api", "beta", "alpha", "git"] BANNED_NAMES = ["tool", "game", "old", "outdated", "wip", "api", "beta", "alpha", "git"]
def getNameFromTaglist(taglist): def getNameFromTaglist(taglist):
for tag in reversed(regex_tag.findall(taglist)): for tag in reversed(regex_tag.findall(taglist)):
if len(tag) < 30 and not tag in BANNED_NAMES and \ if len(tag) < 30 and not tag in BANNED_NAMES and \
@ -112,81 +112,81 @@ def getLinksFromModSearch():
pass pass
except urllib.error.URLError: except urllib.error.URLError:
print("Unable to open krocks mod search!") print("Unable to open krocks tool search!")
return links return links
return links return links
@celery.task() # @celery.task()
def importTopicList(): # def importTopicList():
links_by_id = getLinksFromModSearch() # links_by_id = getLinksFromModSearch()
info_by_id = {} # info_by_id = {}
getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': False }) # getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': False })
getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': True }) # getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': True })
getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False }) # getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False })
getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True }) # getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True })
# Caches # # Caches
username_to_user = {} # username_to_user = {}
topics_by_id = {} # topics_by_id = {}
for topic in ForumTopic.query.all(): # for topic in ForumTopic.query.all():
topics_by_id[topic.topic_id] = topic # topics_by_id[topic.topic_id] = topic
def get_or_create_user(username): # def get_or_create_user(username):
user = username_to_user.get(username) # user = username_to_user.get(username)
if user: # if user:
return user # return user
if not is_username_valid(username): # if not is_username_valid(username):
return None # return None
user = User.query.filter_by(forums_username=username).first() # user = User.query.filter_by(forums_username=username).first()
if user is None: # if user is None:
user = User.query.filter_by(username=username).first() # user = User.query.filter_by(username=username).first()
if user: # if user:
return None # return None
user = User(username) # user = User(username)
user.forums_username = username # user.forums_username = username
db.session.add(user) # db.session.add(user)
username_to_user[username] = user # username_to_user[username] = user
return user # return user
# Create or update # # Create or update
for info in info_by_id.values(): # for info in info_by_id.values():
id = int(info["id"]) # id = int(info["id"])
# Get author # # Get author
username = info["author"] # username = info["author"]
user = get_or_create_user(username) # user = get_or_create_user(username)
if user is None: # if user is None:
print("Error! Unable to create user {}".format(username), file=sys.stderr) # print("Error! Unable to create user {}".format(username), file=sys.stderr)
continue # continue
# Get / add row # # Get / add row
topic = topics_by_id.get(id) # topic = topics_by_id.get(id)
if topic is None: # if topic is None:
topic = ForumTopic() # topic = ForumTopic()
db.session.add(topic) # db.session.add(topic)
# Parse title # # Parse title
title, name = parseTitle(info["title"]) # title, name = parseTitle(info["title"])
# Get link # # Get link
link = links_by_id.get(id) # link = links_by_id.get(id)
# Fill row # # Fill row
topic.topic_id = int(id) # topic.topic_id = int(id)
topic.author = user # topic.author = user
topic.type = info["type"] # topic.type = info["type"]
topic.title = title # topic.title = title
topic.name = name # topic.name = name
topic.link = link # topic.link = link
topic.wip = info["wip"] # topic.wip = info["wip"]
topic.posts = int(info["posts"]) # topic.posts = int(info["posts"])
topic.views = int(info["views"]) # topic.views = int(info["views"])
topic.created_at = info["date"] # topic.created_at = info["date"]
db.session.commit() # db.session.commit()

View File

@ -75,11 +75,8 @@ def getMeta(urlstr, author):
def postReleaseCheckUpdate(self, release: PackageRelease, path): def postReleaseCheckUpdate(self, release: PackageRelease, path):
try: try:
tree = build_tree(path, expected_type=ContentType[release.package.type.name], tree = build_tree(path, author=release.package.author.username, name=release.package.name)
author=release.package.author.username, name=release.package.name)
if tree.name is not None and release.package.name != tree.name and tree.type == ContentType.MOD:
raise MinetestCheckError(f"Expected {tree.relative} to have technical name {release.package.name}, instead has name {tree.name}")
cache = {} cache = {}
def getMetaPackages(names): def getMetaPackages(names):
@ -99,14 +96,9 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
optional_depends = tree.fold("meta", "optional_depends") optional_depends = tree.fold("meta", "optional_depends")
# Filter out provides # Filter out provides
for mod in provides: for tool in provides:
depends.discard(mod) depends.discard(tool)
optional_depends.discard(mod) optional_depends.discard(tool)
# Raise error on unresolved game dependencies
if package.type == PackageType.GAME and len(depends) > 0:
deps = ", ".join(depends)
raise MinetestCheckError("Game has unresolved hard dependencies: " + deps)
# Add dependencies # Add dependencies
for meta in getMetaPackages(depends): for meta in getMetaPackages(depends):
@ -116,16 +108,16 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
db.session.add(Dependency(package, meta=meta, optional=True)) db.session.add(Dependency(package, meta=meta, optional=True))
# Update game supports # Update game supports
if package.type == PackageType.MOD: # if package.type == PackageType.TOOL:
resolver = GameSupportResolver() # resolver = GameSupportResolver()
resolver.update(package) # resolver.update(package)
# Update min/max # # Update min/max
if tree.meta.get("min_minetest_version"): # if tree.meta.get("min_minetest_version"):
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None) # release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
if tree.meta.get("max_minetest_version"): # if tree.meta.get("max_minetest_version"):
release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None) # release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None)
try: try:
with open(os.path.join(tree.baseDir, ".cdb.json"), "r") as f: with open(os.path.join(tree.baseDir, ".cdb.json"), "r") as f:

View File

@ -8,13 +8,13 @@ class MinetestCheckError(Exception):
class ContentType(Enum): class ContentType(Enum):
UNKNOWN = "unknown" UNKNOWN = "unknown"
MOD = "mod" TOOL = "tool"
MODPACK = "modpack" MODPACK = "modpack"
GAME = "game" GAME = "game"
TXP = "texture pack" ASSETPACK = "texture pack"
def isModLike(self): def isModLike(self):
return self == ContentType.MOD or self == ContentType.MODPACK return self == ContentType.TOOL or self == ContentType.MODPACK
def validate_same(self, other): def validate_same(self, other):
""" """
@ -22,18 +22,6 @@ class ContentType(Enum):
""" """
assert other assert other
if self == ContentType.MOD:
if not other.isModLike():
raise MinetestCheckError("Expected a mod or modpack, found " + other.value)
elif self == ContentType.TXP:
if other != ContentType.UNKNOWN and other != ContentType.TXP:
raise MinetestCheckError("expected a " + self.value + ", found a " + other.value)
elif other != self:
raise MinetestCheckError("Expected a " + self.value + ", found a " + other.value)
from .tree import PackageTreeNode, get_base_dir from .tree import PackageTreeNode, get_base_dir
def build_tree(path, expected_type=None, author=None, repo=None, name=None): def build_tree(path, expected_type=None, author=None, repo=None, name=None):

View File

@ -19,14 +19,14 @@ def detect_type(path):
if os.path.isfile(path + "/game.conf"): if os.path.isfile(path + "/game.conf"):
return ContentType.GAME return ContentType.GAME
elif os.path.isfile(path + "/init.lua"): elif os.path.isfile(path + "/init.lua"):
return ContentType.MOD return ContentType.TOOL
elif os.path.isfile(path + "/modpack.txt") or \ elif os.path.isfile(path + "/modpack.txt") or \
os.path.isfile(path + "/modpack.conf"): os.path.isfile(path + "/modpack.conf"):
return ContentType.MODPACK return ContentType.MODPACK
# elif os.path.isdir(path + "/mods"): # elif os.path.isdir(path + "/mods"):
# return ContentType.GAME # return ContentType.GAME
elif os.path.isfile(path + "/texture_pack.conf"): elif os.path.isfile(path + "/texture_pack.conf"):
return ContentType.TXP return ContentType.ASSETPACK
else: else:
return ContentType.UNKNOWN return ContentType.UNKNOWN
@ -53,13 +53,7 @@ class PackageTreeNode:
self.read_meta() self.read_meta()
if self.type == ContentType.GAME: if self.type == ContentType.GAME:
if not os.path.isdir(baseDir + "/mods"):
raise MinetestCheckError("Game at {} does not have a mods/ folder".format(self.relative))
self.add_children_from_mod_dir("mods") self.add_children_from_mod_dir("mods")
elif self.type == ContentType.MOD:
if self.name and not basenamePattern.match(self.name):
raise MinetestCheckError("Invalid base name for mod {} at {}, names must only contain a-z0-9_." \
.format(self.name, self.relative))
elif self.type == ContentType.MODPACK: elif self.type == ContentType.MODPACK:
self.add_children_from_mod_dir(None) self.add_children_from_mod_dir(None)
@ -72,11 +66,11 @@ class PackageTreeNode:
def getMetaFileName(self): def getMetaFileName(self):
if self.type == ContentType.GAME: if self.type == ContentType.GAME:
return "game.conf" return "game.conf"
elif self.type == ContentType.MOD: elif self.type == ContentType.TOOL:
return "mod.conf" return "tool.conf"
elif self.type == ContentType.MODPACK: elif self.type == ContentType.MODPACK:
return "modpack.conf" return "modpack.conf"
elif self.type == ContentType.TXP: elif self.type == ContentType.ASSETPACK:
return "texture_pack.conf" return "texture_pack.conf"
else: else:
return None return None
@ -94,14 +88,7 @@ class PackageTreeNode:
conf = parse_conf(myfile.read()) conf = parse_conf(myfile.read())
for key, value in conf.items(): for key, value in conf.items():
result[key] = value result[key] = value
except SyntaxError as e: except IOError: pass
raise MinetestCheckError("Error while reading {}: {}".format(meta_file_rel , e.msg))
except IOError:
pass
if "release" in result:
raise MinetestCheckError("{} should not contain 'release' key, as this is for use by ContentDB only.".format(meta_file_rel))
# description.txt # description.txt
if not "description" in result: if not "description" in result:
@ -139,16 +126,7 @@ class PackageTreeNode:
result["optional_depends"] = [] result["optional_depends"] = []
def checkDependencies(deps): def checkDependencies(deps): pass
for dep in deps:
if not basenamePattern.match(dep):
if " " in dep:
raise MinetestCheckError("Invalid dependency name '{}' for mod at {}, did you forget a comma?" \
.format(dep, self.relative))
else:
raise MinetestCheckError(
"Invalid dependency name '{}' for mod at {}, names must only contain a-z0-9_." \
.format(dep, self.relative))
# Check dependencies # Check dependencies
checkDependencies(result["depends"]) checkDependencies(result["depends"])
@ -187,17 +165,11 @@ class PackageTreeNode:
path = os.path.join(dir, entry) path = os.path.join(dir, entry)
if not entry.startswith('.') and os.path.isdir(path): if not entry.startswith('.') and os.path.isdir(path):
child = PackageTreeNode(path, relative + entry + "/", name=entry) child = PackageTreeNode(path, relative + entry + "/", name=entry)
if not child.type.isModLike():
raise MinetestCheckError("Expecting mod or modpack, found {} at {} inside {}" \
.format(child.type.value, child.relative, self.type.value))
if child.name is None:
raise MinetestCheckError("Missing base name for mod at {}".format(self.relative))
self.children.append(child) self.children.append(child)
def getModNames(self): def getModNames(self):
return self.fold("name", type=ContentType.MOD) return self.fold("name", type=ContentType.TOOL)
# attr: Attribute name # attr: Attribute name
# key: Key in attribute # key: Key in attribute

View File

@ -11,7 +11,6 @@
<a class="list-group-item list-group-item-action" href="{{ url_for('users.list_all') }}">User list</a> <a class="list-group-item list-group-item-action" href="{{ url_for('users.list_all') }}">User list</a>
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.tag_list') }}">Tag Editor</a> <a class="list-group-item list-group-item-action" href="{{ url_for('admin.tag_list') }}">Tag Editor</a>
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.license_list') }}">License Editor</a> <a class="list-group-item list-group-item-action" href="{{ url_for('admin.license_list') }}">License Editor</a>
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.version_list') }}">Version Editor</a>
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.warning_list') }}">Warning Editor</a> <a class="list-group-item list-group-item-action" href="{{ url_for('admin.warning_list') }}">Warning Editor</a>
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.send_bulk_email') }}">Send bulk email</a> <a class="list-group-item list-group-item-action" href="{{ url_for('admin.send_bulk_email') }}">Send bulk email</a>
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.send_bulk_notification') }}">Send bulk notification</a> <a class="list-group-item list-group-item-action" href="{{ url_for('admin.send_bulk_notification') }}">Send bulk notification</a>

View File

@ -1,23 +1,2 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}
{% if version %}
Edit {{ version.name }}
{% else %}
New Minetest Version
{% endif %}
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_version') }}">New Version</a>
<a class="btn btn-secondary mb-4" href="{{ url_for('admin.version_list') }}">Back to list</a>
{% from "macros/forms.html" import render_field, render_submit_field %}
<form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ render_field(form.name) }}
{{ render_field(form.protocol) }}
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,23 +14,8 @@
<h3>{{ _("Games") }}</h3> <h3>{{ _("Games") }}</h3>
{{ render_pkggrid(mpackage.packages.filter_by(type="GAME", state="APPROVED").all()) }} {{ render_pkggrid(mpackage.packages.filter_by(type="GAME", state="APPROVED").all()) }}
<h3>{{ _("Mods") }}</h3> <h3>{{ _("Tools") }}</h3>
{{ render_pkggrid(mpackage.packages.filter_by(type="MOD", state="APPROVED").all()) }} {{ render_pkggrid(mpackage.packages.filter_by(type="TOOL", state="APPROVED").all()) }}
{% if similar_topics %}
<h3>{{ _("Forum Topics") }}</h3>
<ul>
{% for t in similar_topics %}
<li>
[{{ t.type.text }}]
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
{{ _("%(title)s by %(display_name)s", title=t.title, display_name=t.author.display_name) }}
</a>
{% if t.wip %}[{{ _("WIP") }}]{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
<h2>{{ _("Required By") }}</h2> <h2>{{ _("Required By") }}</h2>
{{ render_pkggrid(dependers) }} {{ render_pkggrid(dependers) }}

View File

@ -46,7 +46,7 @@
<div class="alert alert-secondary"> <div class="alert alert-secondary">
<a class="float-right btn btn-sm btn-default" href="/help/package_config/#cdbjson">{{ _("Read more") }}</a> <a class="float-right btn btn-sm btn-default" href="/help/package_config/#cdbjson">{{ _("Read more") }}</a>
{{ _("You can include a .cdb.json file in your %(type)s to update these details automatically.", type=package.type.text.lower()) }} {{ _("You can include a .cdb.json file in your projects to update these details automatically.") }}
</div> </div>
{% endif %} {% endif %}
@ -72,7 +72,7 @@
{{ render_field(form.name, class_="pkg_meta col-sm-4", {{ render_field(form.name, class_="pkg_meta col-sm-4",
readonly=True, hint=_("Please open a thread to request a name change")) }} readonly=True, hint=_("Please open a thread to request a name change")) }}
{% else %} {% else %}
{{ render_field(form.name, class_="pkg_meta col-sm-4", pattern="[a-z0-9_]+", title=_("Lower case letters (a-z), digits (0-9), and underscores (_) only")) }} {{ render_field(form.name, class_="pkg_meta col-sm-4", pattern="[a-zA-Z0-9_\-\.]+", title=_("Lower case letters (a-z), digits (0-9), and underscores (_), dashes and periods only")) }}
{% endif %} {% endif %}
</div> </div>
{{ render_field(form.short_desc, class_="pkg_meta") }} {{ render_field(form.short_desc, class_="pkg_meta") }}
@ -113,10 +113,6 @@
{{ render_field(form.website, class_="pkg_meta") }} {{ render_field(form.website, class_="pkg_meta") }}
{{ render_field(form.issueTracker, class_="pkg_meta") }} {{ render_field(form.issueTracker, class_="pkg_meta") }}
{{ render_field_prefix_button(form.forums, class_="pkg_meta",
pattern="[0-9]+",
prefix="forum.minetest.net/viewtopic.php?t=",
placeholder=_("Tip: paste in a forum topic URL")) }}
{{ render_field(form.video_url, class_="pkg_meta", hint=_("YouTube videos will be shown in an embed.")) }} {{ render_field(form.video_url, class_="pkg_meta", hint=_("YouTube videos will be shown in an embed.")) }}
</fieldset> </fieldset>

View File

@ -8,8 +8,8 @@
{% block headextra %} {% block headextra %}
<meta name="og:title" content="{{ self.title() }}"/> <meta name="og:title" content="{{ self.title() }}"/>
<meta name="og:description" content="{{ _('Mods for %(title)s', title=package.title) }}"/> <meta name="og:description" content="{{ _('Tools for %(title)s', title=package.title) }}"/>
<meta name="description" content="{{ _('Mods for %(title)s', title=package.title) }}"/> <meta name="description" content="{{ _('Tools for %(title)s', title=package.title) }}"/>
<meta name="og:url" content="{{ package.getURL('packages.game_hub', absolute=True) }}"/> <meta name="og:url" content="{{ package.getURL('packages.game_hub', absolute=True) }}"/>
{% if package.getMainScreenshotURL() %} {% if package.getMainScreenshotURL() %}
<meta name="og:image" content="{{ package.getMainScreenshotURL(absolute=True) }}"/> <meta name="og:image" content="{{ package.getMainScreenshotURL(absolute=True) }}"/>
@ -40,13 +40,13 @@
<h2 class="my-3">{{ _("Recently Updated") }}</h2> <h2 class="my-3">{{ _("Recently Updated") }}</h2>
{{ render_pkggrid(updated) }} {{ render_pkggrid(updated) }}
{% for tag in toplevel %}
<a href="{{ url_for('packages.list_all', type='mod', sort='score', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right"> <a href="{{ url_for('packages.list_all', tag=tag.name, sort='score', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
{{ _("See more") }} {{ _("See more") }}
</a> </a>
<h2 class="my-3">{{ _("Top Mods") }}</h2> <h2 class="my-3">{{ _("Top " + tag.title) }}</h2>
{{ render_pkggrid(pop_mod) }} {{ render_pkggrid(popular[tag.name]) }}
{% endfor %}
<a href="{{ url_for('packages.list_all', sort='reviews', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right"> <a href="{{ url_for('packages.list_all', sort='reviews', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
{{ _("See more") }} {{ _("See more") }}

View File

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

View File

@ -43,25 +43,6 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<h3 class="mt-5">{{ _("Supported Minetest versions") }}</h3>
<div class="row">
{{ render_field(form.min_rel, class_="col-sm-6") }}
{{ render_field(form.max_rel, class_="col-sm-6") }}
</div>
<p id="minmax_warning" style="color:#f00; display: none;">
{{ _("Maximum must be greater than or equal to the minimum!") }}
</p>
<p>
{{ _("Set the minimum and maximum Minetest versions supported.
This release will be hidden to clients outside of that range. ") }}
<br />
{{ _("Leave both as None if in doubt.") }}
{{ _("You can <a href='/help/package_config/'>set this automatically</a> in the .conf of your package.") }}
</p>
<p class="mt-5"> <p class="mt-5">
{{ render_submit_field(form.submit) }} {{ render_submit_field(form.submit) }}
</p> </p>

View File

@ -58,30 +58,6 @@
tips on customising releases.") }} tips on customising releases.") }}
</p> </p>
<h3 class="mt-5">{{ _("3. Supported Minetest versions") }}</h3>
<div class="row">
{{ render_field(form.min_rel, class_="col-sm-6") }}
{{ render_field(form.max_rel, class_="col-sm-6") }}
</div>
<p id="minmax_warning" style="color:#f00; display: none;">
{{ _("Maximum must be greater than or equal to the minimum!") }}
</p>
<p>
<i class="fas fa-exclamation-circle mr-2"></i>
{{ _("The .conf of your package can <a href='/help/package_config/'>set this automatically</a>,
which will override your selection.") }}
</p>
<p>
{{ _("Set the minimum and maximum Minetest versions supported.
This release will be hidden to clients outside of that range. ") }}
<br />
{{ _("Leave both as None if in doubt.") }}
</p>
<p class="mt-5"> <p class="mt-5">
{{ render_submit_field(form.submit) }} {{ render_submit_field(form.submit) }}
</p> </p>

View File

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

View File

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

View File

@ -28,18 +28,4 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if similar_topics %}
<h3>{{ _("Similar Forum Topics") }}</h3>
<ul>
{% for t in similar_topics %}
<li>
[{{ t.type.value }}]
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
{{ _("%(title)s by %(display_name)s", title=t.title, display_name=t.author.display_name) }}
</a>
{% if t.wip %}[{{ _("WIP") }}]{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -37,37 +37,8 @@
{{ _("Download") }} {{ _("Download") }}
</div> </div>
{% if release and (release.min_rel or release.max_rel) %}
<small class="count display-block">
{% if release.min_rel and release.max_rel %}
{{ _("Minetest %(min)s - %(max)s", min=release.min_rel.name, max=release.max_rel.name) }}
{% elif release.min_rel %}
{{ _("For Minetest %(min)s and above", min=release.min_rel.name) }}
{% elif release.max_rel %}
{{ _("Minetest %(max)s and below", max=release.max_rel.name) }}
{% endif %}
</small>
{% endif %}
</a> </a>
<p></p>
{% if package.type == package.type.MOD %}
{% set installing_url = "https://wiki.minetest.net/Installing_Mods" %}
{% elif package.type == package.type.GAME %}
{% set installing_url = "https://wiki.minetest.net/Games#Installing_games" %}
{% elif package.type == package.type.TXP %}
{% set installing_url = "https://wiki.minetest.net/Installing_Texture_Packs" %}
{% else %}
{{ 0 / 0 }}
{% endif %}
<p class="text-center mt-1 mb-4">
<a href="{{ installing_url }}">
<small>
<i class="fas fa-question-circle mr-1"></i>
{{ _("How do I install this?") }}
</small>
</a>
</p>
{% else %} {% else %}
<i> <i>
{{ _("No downloads available") }} {{ _("No downloads available") }}
@ -76,9 +47,9 @@
{% endblock %} {% endblock %}
{% block container %} {% block container %}
{% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.TXP %} {% if not package.license.is_foss and not package.media_license.is_foss %}
{% set package_warning=_("Non-free code and media") %} {% set package_warning=_("Non-free code and media") %}
{% elif not package.license.is_foss and package.type != package.type.TXP %} {% elif not package.license.is_foss %}
{% set package_warning=_("Non-free code") %} {% set package_warning=_("Non-free code") %}
{% elif not package.media_license.is_foss %} {% elif not package.media_license.is_foss %}
{% set package_warning=_("Non-free media") %} {% set package_warning=_("Non-free media") %}
@ -170,7 +141,7 @@
<span class="count">{{ package.downloads }}</span> <span class="count">{{ package.downloads }}</span>
</a> </a>
{% endif %} {% endif %}
<a class="btn" href="{{ url_for('threads.list_all', pid=package.id) }}" title="{{ _("Threads") }}"> <a class="btn" href="{{ url_for('threads.list_all', pid=package.id) }}" title="{{ _("Discussion") }}">
<i class="fas fa-comment-alt"></i> <i class="fas fa-comment-alt"></i>
<span class="count">{{ threads | length }}</span> <span class="count">{{ threads | length }}</span>
</a> </a>
@ -194,12 +165,6 @@
<span class="count">{{ _("Source") }}</span> <span class="count">{{ _("Source") }}</span>
</a> </a>
{% endif %} {% endif %}
{% if package.forums %}
<a class="btn" href="https://forum.minetest.net/viewtopic.php?t={{ package.forums }}">
<i class="fas fa-comments"></i>
<span class="count">{{ _("Forums") }}</span>
</a>
{% endif %}
{% if package.issueTracker %} {% if package.issueTracker %}
<a class="btn" href="{{ package.issueTracker }}"> <a class="btn" href="{{ package.issueTracker }}">
<i class="fas fa-bug"></i> <i class="fas fa-bug"></i>
@ -265,13 +230,13 @@
{% for ss in screenshots %} {% for ss in screenshots %}
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %} {% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
<li> <li>
<a href="{{ ss.url }}" class="gallery-image"> <a href="{{ss.url}}" class="gallery-image" data-toggle="modal" data-target="#screenshot_{{ss.id}}">
<img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" /> <img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
{% if not ss.approved %} {% if not ss.approved %}
<span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span> <span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span>
{% endif %} {% endif %}
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% else %} {% else %}
<li> <li>
@ -290,6 +255,25 @@
</article> </article>
{% endif %} {% endif %}
{% if package.build_desc %}
<article class="markdown panel mb-5">
{{ package.build_desc | markdown }}
</article>
{% endif %}
{% if package.install_desc %}
<div class="card mt-0 mb-4">
<div class="card-header">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#install_desc" aria-expanded="true" aria-controls="install_desc">
{{ _("Installation Instructions") }}
</button>
</div>
<article class="markdown panel mb-5 collapse" id="install_desc">
{{ package.install_desc | markdown }}
</article>
</div>
{% endif %}
<h2 id="reviews" class="mt-0">{{ _("Reviews") }}</h2> <h2 id="reviews" class="mt-0">{{ _("Reviews") }}</h2>
{% from "macros/reviews.html" import render_reviews, render_review_form, render_review_preview with context %} {% from "macros/reviews.html" import render_reviews, render_review_form, render_review_preview with context %}
@ -323,12 +307,10 @@
{{ render_pkggrid(packages_uses) }} {{ render_pkggrid(packages_uses) }}
{% endif %} {% endif %}
{% if package.type == package.type.GAME %} <h2>{{ _("Content") }}</h2>
<h2>{{ _("Content") }}</h2> <a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary">
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary"> {{ _("View content for game") }}
{{ _("View content for game") }} </a>
</a>
{% endif %}
</div> </div>
<aside class="col-md-3 info-sidebar"> <aside class="col-md-3 info-sidebar">
@ -378,92 +360,81 @@
</div> </div>
{% endif %} {% endif %}
{% if package.type == package.type.GAME %} <a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary">
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary"> {{ _("View content for game") }}
{{ _("View content for game") }} </a>
</a> <h3>{{ _("Dependencies") }}</h3>
{% endif %} <dl>
<dt>{{ _("Required") }}</dt>
<dd>
{% for dep in package.getSortedHardDependencies() %}
{%- if dep.package %}
<a class="badge badge-primary"
href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }}
</a>
{% elif dep.meta_package %}
<a class="badge badge-primary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
</a>
{% else %}
{{ "Expected package or meta_package in dep!" | throw }}
{% endif %}
{% else %}
{{ _("No required dependencies") }}
{% endfor %}
</dd>
{% if package.type != package.type.TXP %} {% set optional_deps=package.getSortedOptionalDependencies() %}
<h3>{{ _("Dependencies") }}</h3> {% if optional_deps %}
<dl> <dt>{{ _("Optional") }}</dt>
<dt>{{ _("Required") }}</dt>
<dd> <dd>
{% for dep in package.getSortedHardDependencies() %} {% for dep in optional_deps %}
{%- if dep.package %} {%- if dep.package %}
<a class="badge badge-primary" <a class="badge badge-secondary"
href="{{ dep.package.getURL("packages.view") }}"> href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s", {{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }} title=dep.package.title, display_name=dep.package.author.display_name) }}
</a>
{% elif dep.meta_package %} {% elif dep.meta_package %}
<a class="badge badge-primary" <a class="badge badge-secondary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}"> href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }} {{ dep.meta_package.name }}
</a>
{% else %} {% else %}
{{ "Expected package or meta_package in dep!" | throw }} {{ "Expected package or meta_package in dep!" | throw }}
{% endif %} {% endif %}</a>
{% else %}
{{ _("No required dependencies") }}
{% endfor %} {% endfor %}
</dd> </dd>
{% endif %}
</dl>
{% set optional_deps=package.getSortedOptionalDependencies() %} <h3>{{ _("Compatible Games") }}</h3>
{% if optional_deps %} {% for support in package.getSortedSupportedGames() %}
<dt>{{ _("Optional") }}</dt> <a class="badge badge-secondary"
<dd> href="{{ support.game.getURL('packages.view') }}">
{% for dep in optional_deps %} {{ _("%(title)s by %(display_name)s",
{%- if dep.package %} title=support.game.title, display_name=support.game.author.display_name) }}
<a class="badge badge-secondary" </a>
href="{{ dep.package.getURL("packages.view") }}"> {% else %}
{{ _("%(title)s by %(display_name)s", {{ _("No specific game is required") }}
title=dep.package.title, display_name=dep.package.author.display_name) }} {% endfor %}
{% elif dep.meta_package %} <p class="text-muted small mt-2 mb-0">
<a class="badge badge-secondary" {{ _("This is an experimental feature.") }}
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}"> {{ _("Supported games are determined by an algorithm, and may not be correct.") }}
{{ dep.meta_package.name }} </p>
{% else %}
{{ "Expected package or meta_package in dep!" | throw }}
{% endif %}</a>
{% endfor %}
</dd>
{% endif %}
</dl>
{% endif %}
{% if package.type == package.type.MOD %}
<h3>{{ _("Compatible Games") }}</h3>
{% for support in package.getSortedSupportedGames() %}
<a class="badge badge-secondary"
href="{{ support.game.getURL('packages.view') }}">
{{ _("%(title)s by %(display_name)s",
title=support.game.title, display_name=support.game.author.display_name) }}
</a>
{% else %}
{{ _("No specific game is required") }}
{% endfor %}
<p class="text-muted small mt-2 mb-0">
{{ _("This is an experimental feature.") }}
{{ _("Supported games are determined by an algorithm, and may not be correct.") }}
</p>
{% endif %}
<h3> <h3>
{{ _("Information") }} {{ _("Information") }}
</h3> </h3>
<dl> <dl>
<dt>{{ _("Type") }}</dt>
<dd>{{ package.type.text }}</dd>
<dt>{{ _("Technical Name") }}</dt> <dt>{{ _("Technical Name") }}</dt>
<dd>{{ package.name }}</dd> <dd>{{ package.name }}</dd>
<dt>{{ _("License") }}</dt> <dt>{{ _("License") }}</dt>
<dd> <dd>
{% if package.license == package.media_license %} {% if package.license == package.media_license %}
{{ render_license(package.license) }} {{ render_license(package.license) }}
{% elif package.type == package.type.TXP %}
{{ render_license(package.media_license) }}
{% else %} {% else %}
{{ _("%(code_license)s for code,<br>%(media_license)s for media.", {{ _("%(code_license)s for code,<br>%(media_license)s for media.",
code_license=render_license(package.license), media_license=render_license(package.media_license)) }} code_license=render_license(package.license), media_license=render_license(package.media_license)) }}
@ -526,7 +497,7 @@
<a class="btn btn-primary btn-sm mx-1" href="{{ url_for('threads.new', pid=package.id) }}"><i class="fas fa-plus"></i></a> <a class="btn btn-primary btn-sm mx-1" href="{{ url_for('threads.new', pid=package.id) }}"><i class="fas fa-plus"></i></a>
</div> </div>
{% endif %} {% endif %}
{{ _("Threads") }} {{ _("Discussion") }}
</h3> </h3>
<div class="list-group"> <div class="list-group">
{% from "macros/threads.html" import render_compact_threadlist %} {% from "macros/threads.html" import render_compact_threadlist %}
@ -552,5 +523,25 @@
</aside> </aside>
</div> </div>
</section> </section>
{% for ss in screenshots %}
<div class="modal fade" id="screenshot_{{ss.id}}" tabindex="-1" aria-labelledby="screenshot" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="screenshot title">"{{ ss.title }}"</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<img src="{{ ss.url }}" alt="{{ ss.title }}" style="width: 100%"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endfor %}
</main> </main>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
{{ _("Threads") }} {{ _("Discussion") }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -9,11 +9,11 @@
<div class="btn-group btn-group-sm mr-2"> <div class="btn-group btn-group-sm mr-2">
{% if is_mtm_only %} {% if is_mtm_only %}
<a class="btn btn-sm btn-primary active" href="{{ url_set_query(mtm=0) }}"> <a class="btn btn-sm btn-primary active" href="{{ url_set_query(mtm=0) }}">
{{ _("Minetest-Mods org only") }} {{ _("Minetest-Tools org only") }}
</a> </a>
{% else %} {% else %}
<a class="btn btn-sm btn-secondary" href="{{ url_set_query(mtm=1) }}"> <a class="btn btn-sm btn-secondary" href="{{ url_set_query(mtm=1) }}">
{{ _("Minetest-Mods org only") }} {{ _("Minetest-Tools org only") }}
</a> </a>
{% endif %} {% endif %}
</div> </div>

View File

@ -29,12 +29,6 @@
{{ _("Package Tags") }} {{ _("Package Tags") }}
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link {% if current_tab == "topics" %}active{% endif %}"
href="{{ url_for('todo.topics') }}">
{{ _("Forum Topics") }}
</a>
</li>
</ul> </ul>
</div> </div>
</nav> </nav>

View File

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

View File

@ -161,13 +161,6 @@
<p> <p>
{{ medal.description }} {{ medal.description }}
</p> </p>
<div class="progress">
<div class="progress-bar" role="progressbar"
style="width: {{ [100 * medal.progress[0] / medal.progress[1], 100] | min }}%;"
aria-valuenow="{{ medal.progress[0] }}" aria-valuemin="0" aria-valuemax="{{ medal.progress[1] }}">
{{ _("%(value)d / %(target)d", value=medal.progress[0], target=medal.progress[1]) }}
</div>
</div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View File

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

View File

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

View File

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

72
app/utils/lists.py Normal file
View File

@ -0,0 +1,72 @@
badLicenses = [
'LicenseRef-proprietary',
'LicenseRef-Proprietary',
'proprietary',
'Proprietary',
'CC-BY-NC-SA-3.0',
'CC-BY-NC-ND-3.0'
]
badCategories = [
'Emulator',
'PackageManager',
'System',
'Utility'
]
nonFreeAssets = [
'jp.yvt.OpenSpades',
'net.openra.OpenRA',
'org.openmw.OpenMW',
'org.zdoom.GZDoom',
'io.github.ezQuake',
'com.etlegacy.ETLegacy',
'com.github.iortcw.iortcw',
'org.yamagi.YamagiQ2',
'org.dhewm3.Dhewm3',
'com.github.bvschaik.julius',
'io.openrct2.OpenRCT2',
'com.github.skullernet.q2pro',
'org.raceintospace.Raceintospace',
'org.srb2.SRB2',
'org.srb2.SRB2Kart',
'io.sourceforge.clonekeenplus',
'io.github.fabiangreffrath.Doom',
'net.dengine.Doomsday',
'com.github.keriew.augustus',
'io.github.yairm210.unciv',
'com.corsixth.corsixth'
]
nonFreeNetworkServices = [
'io.github.yairm210.unciv'
]
alwaysAccept = [
'org.freecol.FreeCol',
'org.freeciv.Freeciv',
'io.github.EndlessSky.endless-sky',
'org.kde.ksudoku',
'net.veloren.veloren'
]
alwaysDeny = [
'com.moonlight_stream.Moonlight',
'org.gnome.Games',
'org.ppsspp.PPSSPP',
'org.scummvm.ScummVM',
'org.pegasus_frontend.Pegasus',
'com.gitlab.coringao.cavestory-nx',
'org.sauerbraten.Sauerbraten',
'net.runelite.RuneLite',
'com.zandronum.Zandronum',
'io.mrarm.mcpelauncher',
'org.unitystation.StationHub',
'org.firestormviewer.FirestormViewer',
'com.eduke32.EDuke32',
'io.github.hmlendea.geforcenow-electron',
'io.gdevs.GDLauncher',
'io.github.sharkwouter.Minigalaxy',
'com.katawa_shoujo.KatawaShoujo',
're.chiaki.Chiaki'
]

View File

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

78
docker-compose.debug.yml Normal file
View File

@ -0,0 +1,78 @@
version: '3'
services:
db:
image: "postgres:14.1"
volumes:
- "./data/db:/var/lib/postgresql/data"
environment: &env
- POSTGRES_USER=contentdb
- POSTGRES_PASSWORD=password
- POSTGRES_DB=contentdb
- FLASK_DEBUG=1
- FLASK_CONFIG=../config.cfg
adminer:
image: adminer
restart: always
environment:
- ADMINER_DEFAULT_SERVER=db
- ADMINER_DESIGN=pepa-linha
ports:
- 8081:8080
redis:
image: 'redis:6.2-alpine'
command: redis-server
volumes:
- './data/redis:/data'
redis-commander:
container_name: redis-commander
hostname: redis-commander
image: rediscommander/redis-commander:latest
restart: always
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- "8082:8081"
app:
build: .
container_name: contentdb_app_1
command: ./utils/entrypoint.sh
environment: *env
ports:
- 5123:5123
volumes:
- "./data/uploads:/var/cdb/uploads"
- "./app:/source/app"
- "./migrations:/source/migrations"
depends_on:
- db
- redis
worker:
build: .
command: celery -A app.tasks.celery worker --concurrency 1
environment: *env
volumes:
- "./data/uploads:/var/cdb/uploads"
- "./app:/home/cdb/app"
depends_on:
- redis
beat:
build: .
command: celery -A app.tasks.celery beat
environment: *env
depends_on:
- redis
exporter:
image: ovalmoney/celery-exporter
environment: *env
ports:
- 5125:9540
depends_on:
- redis

View File

@ -1,11 +1,17 @@
version: '3' version: '3'
services: services:
db: db:
image: "postgres:14.1" image: "postgres:14.1"
volumes: volumes:
- "./data/db:/var/lib/postgresql/data" - "./data/db:/var/lib/postgresql/data"
env_file: environment: &env
- config.env - POSTGRES_USER=contentdb
- POSTGRES_PASSWORD=password
- POSTGRES_DB=contentdb
- FLASK_DEBUG=0
- FLASK_CONFIG=../config.cfg
redis: redis:
image: 'redis:6.2-alpine' image: 'redis:6.2-alpine'
@ -15,9 +21,9 @@ services:
app: app:
build: . build: .
container_name: contentdb_app_1
command: ./utils/entrypoint.sh command: ./utils/entrypoint.sh
env_file: environment: *env
- config.env
ports: ports:
- 5123:5123 - 5123:5123
volumes: volumes:
@ -31,10 +37,7 @@ services:
worker: worker:
build: . build: .
command: celery -A app.tasks.celery worker --concurrency 1 command: celery -A app.tasks.celery worker --concurrency 1
env_file: environment: *env
- config.env
environment:
- FLASK_CONFIG=../config.cfg
volumes: volumes:
- "./data/uploads:/var/cdb/uploads" - "./data/uploads:/var/cdb/uploads"
- "./app:/home/cdb/app" - "./app:/home/cdb/app"
@ -44,17 +47,13 @@ services:
beat: beat:
build: . build: .
command: celery -A app.tasks.celery beat command: celery -A app.tasks.celery beat
env_file: environment: *env
- config.env
environment:
- FLASK_CONFIG=../config.cfg
depends_on: depends_on:
- redis - redis
exporter: exporter:
image: ovalmoney/celery-exporter image: ovalmoney/celery-exporter
env_file: environment: *env
- config.env
ports: ports:
- 5125:9540 - 5125:9540
depends_on: depends_on:

View File

@ -13,7 +13,7 @@ The query arguments will include a list of supported types, the current
and any hidden [Content Flags](https://content.minetest.net/help/content_flags/). and any hidden [Content Flags](https://content.minetest.net/help/content_flags/).
Example URL: Example URL:
<https://content.minetest.net/api/packages/?type=mod&type=game&type=txp&protocol_version=39&engine_version=5.3.0&hide=nonfree&hide=desktop_default> <https://content.minetest.net/api/packages/?type=tool&type=game&type=asset_pack&protocol_version=39&engine_version=5.3.0&hide=nonfree&hide=desktop_default>
Example response: Example response:
@ -33,7 +33,7 @@ Example response:
`thumbnail` is optional, but all other fields are required. `thumbnail` is optional, but all other fields are required.
`type` is one of `mod`, `game`, or `txp`. `type` is one of `tool`, `game`, or `asset_pack`.
`release` is the release ID. Newer releases have higher IDs. `release` is the release ID. Newer releases have higher IDs.
Minetest compares this ID to a locally stored version to detect whether a package has updates. Minetest compares this ID to a locally stored version to detect whether a package has updates.
@ -67,7 +67,7 @@ dependencies for a package.
Then, it resolves each dependency recursively. Then, it resolves each dependency recursively.
Say you're resolving for `basic_materials`, then it will attempt to find the mod in this order: Say you're resolving for `basic_materials`, then it will attempt to find the tool in this order:
1. It first checks installed mods in the game and mods folder (ie: `mods/basic_materials/`) 1. It first checks installed mods in the game and mods folder (ie: `mods/basic_materials/`)
2. Then it looks on ContentDB for exact name matches (ie: `VanessaE/basic_materials`) 2. Then it looks on ContentDB for exact name matches (ie: `VanessaE/basic_materials`)
@ -77,7 +77,7 @@ Say you're resolving for `basic_materials`, then it will attempt to find the mod
### Long version ### Long version
When installing a package, an API request is made to ContentDB to find out the dependencies. When installing a package, an API request is made to ContentDB to find out the dependencies.
If there are no dependencies, then the mod is installed straight away. If there are no dependencies, then the tool is installed straight away.
If there are dependencies, it will resolve them and show a dialog with a list of mods to install. If there are dependencies, it will resolve them and show a dialog with a list of mods to install.

View File

@ -1,25 +0,0 @@
"""empty message
Revision ID: 011e42c52d21
Revises: 6e57b2b4dcdf
Create Date: 2022-01-25 18:48:46.367409
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '011e42c52d21'
down_revision = '6e57b2b4dcdf'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('package', sa.Column('video_url', sa.String(length=200), nullable=True))
def downgrade():
op.drop_column('package', 'video_url')

View File

@ -1,29 +0,0 @@
"""empty message
Revision ID: 019da77ba02d
Revises: 4f2e19bc2a27
Create Date: 2020-07-09 04:07:23.926213
"""
from alembic import op
import sqlalchemy as sa
import datetime
# revision identifiers, used by Alembic.
revision = '019da77ba02d'
down_revision = '4f2e19bc2a27'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('package_review', sa.Column('created_at', sa.DateTime(), nullable=False, server_default=datetime.datetime.utcnow().isoformat()))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('package_review', 'created_at')
# ### end Alembic commands ###

View File

@ -1,35 +0,0 @@
"""empty message
Revision ID: 06d23947e7ef
Revises: 5d7233cf8a00
Create Date: 2020-12-05 20:30:12.166357
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '06d23947e7ef'
down_revision = '5d7233cf8a00'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('email_subscription',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=100), nullable=False),
sa.Column('blacklisted', sa.Boolean(), nullable=False),
sa.Column('token', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('email_subscription')
# ### end Alembic commands ###

View File

@ -1,30 +0,0 @@
"""empty message
Revision ID: 105d4c740ad6
Revises: 886c92dc6eaa
Create Date: 2020-12-15 17:28:56.559801
"""
import datetime
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
from sqlalchemy import orm
from app.models import User, UserRank
revision = '105d4c740ad6'
down_revision = '886c92dc6eaa'
branch_labels = None
depends_on = None
def upgrade():
op.execute("COMMIT")
op.execute("ALTER TYPE userrank ADD VALUE 'BOT' AFTER 'EDITOR'")
def downgrade():
pass

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 11b6ef362f98
Revises: 9fc23495713b
Create Date: 2018-07-04 01:01:45.440662
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '11b6ef362f98'
down_revision = '9fc23495713b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('package', sa.Column('score', sa.Float(), nullable=False, server_default="0.0"))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('package', 'score')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 13113e5710da
Revises: ead35f7d446c
Create Date: 2018-05-23 20:18:07.606646
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '13113e5710da'
down_revision = 'ead35f7d446c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('package', sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('package', 'created_at')
# ### end Alembic commands ###

View File

@ -1,27 +0,0 @@
"""empty message
Revision ID: 17b303f33f68
Revises: 96a01fe23389
Create Date: 2021-12-20 19:48:58.571336
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '17b303f33f68'
down_revision = '96a01fe23389'
branch_labels = None
depends_on = None
def upgrade():
status = postgresql.ENUM('WIP', 'BETA', 'ACTIVELY_DEVELOPED', 'MAINTENANCE_ONLY', 'AS_IS', 'DEPRECATED', 'LOOKING_FOR_MAINTAINER', name='packagedevstate')
status.create(op.get_bind())
op.add_column('package', sa.Column('dev_state', sa.Enum('WIP', 'BETA', 'ACTIVELY_DEVELOPED', 'MAINTENANCE_ONLY', 'AS_IS', 'DEPRECATED', 'LOOKING_FOR_MAINTAINER', name='packagedevstate'), nullable=True))
def downgrade():
op.drop_column('package', 'dev_state')

View File

@ -1,25 +0,0 @@
"""empty message
Revision ID: 1af840af0209
Revises: 725ff70ea316
Create Date: 2021-08-16 17:17:12.060257
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '1af840af0209'
down_revision = '725ff70ea316'
branch_labels = None
depends_on = None
def upgrade():
op.execute("COMMIT")
op.execute("ALTER TYPE userrank ADD VALUE 'APPROVER' BEFORE 'EDITOR'")
def downgrade():
pass

View File

@ -1,25 +0,0 @@
"""empty message
Revision ID: 28a427cbd4cf
Revises: e9f534df23a8
Create Date: 2018-06-03 01:47:33.006039
"""
# revision identifiers, used by Alembic.
revision = '28a427cbd4cf'
down_revision = 'e9f534df23a8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,35 +0,0 @@
"""empty message
Revision ID: 2f3c3597c78d
Revises: 9ec17b558413
Create Date: 2019-01-29 02:43:08.865695
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy_searchable import sync_trigger
from sqlalchemy_utils.types import TSVectorType
# revision identifiers, used by Alembic.
revision = '2f3c3597c78d'
down_revision = '9ec17b558413'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('package', 'shortDesc', nullable=False, new_column_name='short_desc')
op.add_column('package', sa.Column('search_vector', TSVectorType("title", "short_desc", "desc"), nullable=True))
op.create_index('ix_package_search_vector', 'package', ['search_vector'], unique=False, postgresql_using='gin')
conn = op.get_bind()
sync_trigger(conn, 'package', 'search_vector', ["title", "short_desc", "desc"])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_package_search_vector', table_name='package')
op.drop_column('package', 'search_vector')
# ### end Alembic commands ###

View File

@ -1,24 +0,0 @@
"""empty message
Revision ID: 306ce331a2a7
Revises: 6dca6eceb04d
Create Date: 2020-01-18 23:00:40.487425
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = '306ce331a2a7'
down_revision = '6dca6eceb04d'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
op.create_check_constraint("CK_approval_valid", "package_release", "not approved OR (task_id IS NULL AND NOT url = '')")
def downgrade():
conn = op.get_bind()
op.drop_constraint("CK_approval_valid", "package_release", type_="check")

View File

@ -1,54 +0,0 @@
"""empty message
Revision ID: 3710e5fbbe87
Revises: f6ef5f35abca
Create Date: 2022-01-27 18:50:11.705061
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '3710e5fbbe87'
down_revision = 'f6ef5f35abca'
branch_labels = None
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)
def downgrade():
op.execute('DROP FUNCTION public.parse_websearch(regconfig, text);')
op.execute('DROP FUNCTION public.parse_websearch(text);')

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 3a24fc02365e
Revises: b370c3eb4227
Create Date: 2020-07-17 20:58:31.130449
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3a24fc02365e'
down_revision = 'b370c3eb4227'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tag', sa.Column('description', sa.String(length=500), nullable=True, server_default=None))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('tag', 'description')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 3f4d7cd8401f
Revises: 13113e5710da
Create Date: 2018-05-25 17:53:13.215127
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = '3f4d7cd8401f'
down_revision = '13113e5710da'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
conn.execute("ALTER TYPE packagepropertykey ADD VALUE 'harddeps'")
conn.execute("ALTER TYPE packagepropertykey ADD VALUE 'softdeps'")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,37 +0,0 @@
"""empty message
Revision ID: 3f5836a3df5c
Revises: b3c7ff6655af
Create Date: 2020-12-04 22:30:33.420071
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3f5836a3df5c'
down_revision = 'b3c7ff6655af'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column('user', 'password',
existing_type=sa.VARCHAR(length=255),
nullable=True,
existing_server_default=sa.text("''::character varying"))
op.execute("""
UPDATE "user" SET password=NULL WHERE password=''
""")
op.create_check_constraint("CK_password", "user",
"password IS NULL OR password != ''")
def downgrade():
op.drop_constraint("CK_password", "user", type_="check")
op.alter_column('user', 'password',
existing_type=sa.VARCHAR(length=255),
nullable=False,
existing_server_default=sa.text("''::character varying"))

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 43dc7dbf64c8
Revises: c1ea65e2b492
Create Date: 2020-12-09 19:06:11.891807
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '43dc7dbf64c8'
down_revision = 'c1ea65e2b492'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column('audit_log_entry', 'causer_id',
existing_type=sa.INTEGER(),
nullable=True)
def downgrade():
op.alter_column('audit_log_entry', 'causer_id',
existing_type=sa.INTEGER(),
nullable=False)

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 44e138485931
Revises: 9e2ac631efb0
Create Date: 2018-07-28 14:45:28.879331
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '44e138485931'
down_revision = '9e2ac631efb0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('package_release', sa.Column('commit_hash', sa.String(length=41), nullable=True, server_default=None))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('package_release', 'commit_hash')
# ### end Alembic commands ###

View File

@ -1,24 +0,0 @@
"""empty message
Revision ID: 4585ce5147b8
Revises: 105d4c740ad6
Create Date: 2020-12-15 21:35:18.982716
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '4585ce5147b8'
down_revision = '105d4c740ad6'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('package_update_config', sa.Column('outdated', sa.Boolean(), nullable=False, server_default="false"))
def downgrade():
op.drop_column('package_update_config', 'outdated')

View File

@ -1,39 +0,0 @@
"""empty message
Revision ID: 4e482c47e519
Revises: 900758871713
Create Date: 2018-05-27 22:38:16.507155
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4e482c47e519'
down_revision = '900758871713'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('dependency',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('depender_id', sa.Integer(), nullable=True),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('meta_package_id', sa.Integer(), nullable=True),
sa.Column('optional', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['depender_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['meta_package_id'], ['meta_package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('depender_id', 'package_id', 'meta_package_id', name='_dependency_uc')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('dependency')
# ### end Alembic commands ###

View File

@ -1,40 +0,0 @@
"""empty message
Revision ID: 4f2e19bc2a27
Revises: dd27f1311a90
Create Date: 2020-07-09 00:35:35.066719
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4f2e19bc2a27'
down_revision = 'dd27f1311a90'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('package_review',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('recommends', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('thread', sa.Column('review_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'thread', 'package_review', ['review_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'thread', type_='foreignkey')
op.drop_column('thread', 'review_id')
op.drop_table('package_review')
# ### end Alembic commands ###

View File

@ -1,35 +0,0 @@
"""empty message
Revision ID: 51be0401bb85
Revises: d4262fb15b37
Create Date: 2021-07-24 00:25:04.706191
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '51be0401bb85'
down_revision = 'd4262fb15b37'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('package_alias',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=False),
sa.Column('author', sa.String(length=50), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('package_alias')
# ### end Alembic commands ###

View File

@ -1,42 +0,0 @@
"""empty message
Revision ID: 5d7233cf8a00
Revises: 81de25b72f66
Create Date: 2020-12-05 03:50:18.843494
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5d7233cf8a00'
down_revision = '81de25b72f66'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user_notification_preferences',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('pref_other', sa.Integer(), nullable=False),
sa.Column('pref_package_edit', sa.Integer(), nullable=False),
sa.Column('pref_package_approval', sa.Integer(), nullable=False),
sa.Column('pref_new_thread', sa.Integer(), nullable=False),
sa.Column('pref_new_review', sa.Integer(), nullable=False),
sa.Column('pref_thread_reply', sa.Integer(), nullable=False),
sa.Column('pref_maintainer', sa.Integer(), nullable=False),
sa.Column('pref_editor_alert', sa.Integer(), nullable=False),
sa.Column('pref_editor_misc', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_notification_preferences')
# ### end Alembic commands ###

View File

@ -1,55 +0,0 @@
"""empty message
Revision ID: 605b3d74ada1
Revises: 28a427cbd4cf
Create Date: 2018-06-11 22:50:36.828818
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '605b3d74ada1'
down_revision = '28a427cbd4cf'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('thread',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('private', sa.Boolean(), server_default='0', nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('thread_reply',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('thread_id', sa.Integer(), nullable=False),
sa.Column('comment', sa.String(length=500), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('package', sa.Column('review_thread_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'package', 'thread', ['review_thread_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'package', type_='foreignkey')
op.drop_constraint(None, 'package', type_='foreignkey')
op.drop_column('package', 'review_thread_id')
op.drop_table('thread_reply')
op.drop_table('thread')
# ### end Alembic commands ###

View File

@ -1,22 +0,0 @@
"""empty message
Revision ID: 64fee8e5ab34
Revises: 306ce331a2a7
Create Date: 2020-01-19 02:28:05.432244
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = '64fee8e5ab34'
down_revision = '306ce331a2a7'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column('user', 'confirmed_at', nullable=False, new_column_name='email_confirmed_at')
def downgrade():
op.alter_column('user', 'email_confirmed_at', nullable=False, new_column_name='confirmed_at')

View File

@ -0,0 +1,431 @@
"""empty message
Revision ID: 668167a0e2d8
Revises:
Create Date: 2022-02-25 02:30:06.547144
"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
# revision identifiers, used by Alembic.
revision = '668167a0e2d8'
down_revision = None
branch_labels = None
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),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('description', sa.String(length=500), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('email_subscription',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=100), nullable=False),
sa.Column('blacklisted', sa.Boolean(), nullable=False),
sa.Column('token', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_table('license',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=False),
sa.Column('is_foss', sa.Boolean(), nullable=False),
sa.Column('url', sa.String(length=128), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('meta_package',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('username', sa.String(length=50), nullable=False),
sa.Column('password', sa.String(length=255), nullable=True),
sa.Column('reset_password_token', sa.String(length=100), server_default='', nullable=False),
sa.Column('rank', sa.Enum('BANNED', 'NOT_JOINED', 'NEW_MEMBER', 'MEMBER', 'TRUSTED_MEMBER', 'APPROVER', 'EDITOR', 'BOT', 'MODERATOR', 'ADMIN', name='userrank'), nullable=False),
sa.Column('github_username', sa.String(length=50), nullable=True),
sa.Column('forums_username', sa.String(length=50), nullable=True),
sa.Column('github_access_token', sa.String(length=50), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('email_confirmed_at', sa.DateTime(), nullable=True),
sa.Column('locale', sa.String(length=10), nullable=True),
sa.Column('profile_pic', sa.String(length=255), nullable=True),
sa.Column('is_active', sa.Boolean(), server_default='0', nullable=False),
sa.Column('display_name', sa.String(length=100), nullable=False),
sa.Column('website_url', sa.String(length=255), nullable=True),
sa.Column('donate_url', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('forums_username'),
sa.UniqueConstraint('github_username')
)
op.create_table('thread',
sa.Column('id', sa.Integer(), nullable=False),
# sa.Column('package_id', sa.Integer(), nullable=True),
# sa.Column('review_id', sa.Integer(), nullable=True),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('private', sa.Boolean(), server_default='0', nullable=False),
sa.Column('locked', sa.Boolean(), server_default='0', nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
# sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
# sa.ForeignKeyConstraint(['review_id'], ['package_review.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('package',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('name', sa.Unicode(length=100), nullable=False),
sa.Column('title', sa.Unicode(length=100), nullable=False),
sa.Column('short_desc', sa.Unicode(length=200), nullable=False),
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('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),
sa.Column('license_id', sa.Integer(), nullable=False),
sa.Column('media_license_id', sa.Integer(), nullable=False),
sa.Column('state', sa.Enum('WIP', 'CHANGES_NEEDED', 'READY_FOR_REVIEW', 'APPROVED', 'DELETED', name='packagestate'), nullable=False),
sa.Column('dev_state', sa.Enum('WIP', 'BETA', 'ACTIVELY_DEVELOPED', 'MAINTENANCE_ONLY', 'AS_IS', 'DEPRECATED', 'LOOKING_FOR_MAINTAINER', name='packagedevstate'), nullable=True),
sa.Column('score', sa.Float(), nullable=False),
sa.Column('score_downloads', sa.Float(), nullable=False),
sa.Column('downloads', sa.Integer(), nullable=False),
sa.Column('review_thread_id', sa.Integer(), nullable=True),
sa.Column('repo', sa.String(length=200), nullable=True),
sa.Column('website', sa.String(length=200), nullable=True),
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.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['license_id'], ['license.id'], ),
sa.ForeignKeyConstraint(['media_license_id'], ['license.id'], ),
sa.ForeignKeyConstraint(['review_thread_id'], ['thread.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('package_review',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('recommends', sa.Boolean(), nullable=False),
sa.Column('score', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('thread', sa.Column('package_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'thread', 'package', ['package_id'], ['id'])
op.add_column('thread', sa.Column('review_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'thread', 'package_review', ['review_id'], ['id'] )
op.create_index('ix_package_search_vector', 'package', ['search_vector'], unique=False, postgresql_using='gin')
op.create_table('package_screenshot',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=False),
sa.Column('order', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('url', sa.String(length=100), nullable=False),
sa.Column('approved', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('width', sa.Integer(), nullable=False),
sa.Column('height', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id')
)
# sa.Column('cover_image_id', sa.Integer(), nullable=True),
# sa.ForeignKeyConstraint(['cover_image_id'], ['package_screenshot.id'], ),
op.add_column('package', sa.Column('cover_image_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'package', 'package_screenshot', ['cover_image_id'], ['id'])
op.create_table('tag',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('description', sa.String(length=500), nullable=True),
sa.Column('backgroundColor', sa.String(length=6), nullable=False),
sa.Column('textColor', sa.String(length=6), nullable=False),
sa.Column('views', sa.Integer(), nullable=False),
sa.Column('is_protected', sa.Boolean(), nullable=False),
sa.Column('is_toplevel', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
op.create_table('api_token',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('access_token', sa.String(length=34), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('owner_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('access_token')
)
op.create_table('audit_log_entry',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('causer_id', sa.Integer(), nullable=True),
sa.Column('severity', sa.Enum('NORMAL', 'USER', 'EDITOR', 'MODERATION', name='auditseverity'), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('url', sa.String(length=200), nullable=True),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['causer_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('content_warnings',
sa.Column('content_warning_id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['content_warning_id'], ['content_warning.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('content_warning_id', 'package_id')
)
op.create_table('dependency',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('depender_id', sa.Integer(), nullable=True),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('meta_package_id', sa.Integer(), nullable=True),
sa.Column('optional', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['depender_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['meta_package_id'], ['meta_package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('depender_id', 'package_id', 'meta_package_id', name='_dependency_uc')
)
op.create_table('forum_topic',
sa.Column('topic_id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('wip', sa.Boolean(), nullable=False),
sa.Column('discarded', sa.Boolean(), nullable=False),
sa.Column('type', sa.Enum('GAME', 'TOOL', 'ASSETPACK', name='packagetype'), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('name', sa.String(length=30), nullable=True),
sa.Column('link', sa.String(length=200), nullable=True),
sa.Column('posts', sa.Integer(), nullable=False),
sa.Column('views', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('topic_id')
)
op.create_table('maintainers',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('user_id', 'package_id')
)
op.create_table('notification',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('causer_id', sa.Integer(), nullable=False),
sa.Column('type', sa.Enum('PACKAGE_EDIT', 'PACKAGE_APPROVAL', 'NEW_THREAD', 'NEW_REVIEW', 'THREAD_REPLY', 'BOT', 'MAINTAINER', 'EDITOR_ALERT', 'EDITOR_MISC', 'OTHER', name='notificationtype'), nullable=False),
sa.Column('emailed', sa.Boolean(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('url', sa.String(length=200), nullable=True),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['causer_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('package_alias',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=False),
sa.Column('author', sa.String(length=50), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('package_game_support',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=False),
sa.Column('game_id', sa.Integer(), nullable=False),
sa.Column('supports', sa.Boolean(), nullable=False),
sa.Column('confidence', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['game_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('game_id', 'package_id', name='_package_game_support_uc')
)
op.create_table('package_release',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('releaseDate', sa.DateTime(), nullable=False),
sa.Column('url', sa.String(length=200), nullable=False),
sa.Column('approved', sa.Boolean(), nullable=False),
sa.Column('task_id', sa.String(length=37), nullable=True),
sa.Column('commit_hash', sa.String(length=41), nullable=True),
sa.Column('downloads', sa.Integer(), nullable=False),
sa.Column('channel', sa.String(length=200), nullable=False),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('package_review_vote',
sa.Column('review_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('is_positive', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['review_id'], ['package_review.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('review_id', 'user_id')
)
op.create_table('package_update_config',
sa.Column('package_id', sa.Integer(), nullable=False),
sa.Column('last_commit', sa.String(length=41), nullable=True),
sa.Column('last_tag', sa.String(length=41), nullable=True),
sa.Column('outdated_at', sa.DateTime(), nullable=True),
sa.Column('trigger', sa.Enum('COMMIT', 'TAG', name='packageupdatetrigger'), nullable=False),
sa.Column('ref', sa.String(length=41), nullable=True),
sa.Column('make_release', sa.Boolean(), nullable=False),
sa.Column('auto_created', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('package_id')
)
op.create_table('provides',
sa.Column('package_id', sa.Integer(), nullable=False),
sa.Column('metapackage_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['metapackage_id'], ['meta_package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('package_id', 'metapackage_id')
)
op.create_table('tags',
sa.Column('tag_id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ),
sa.PrimaryKeyConstraint('tag_id', 'package_id')
)
op.create_table('thread_reply',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('thread_id', sa.Integer(), nullable=False),
sa.Column('comment', sa.String(length=2000), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user_email_verification',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=100), nullable=False),
sa.Column('token', sa.String(length=32), nullable=True),
sa.Column('is_password_reset', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user_notification_preferences',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('pref_package_edit', sa.Integer(), nullable=False),
sa.Column('pref_package_approval', sa.Integer(), nullable=False),
sa.Column('pref_new_thread', sa.Integer(), nullable=False),
sa.Column('pref_new_review', sa.Integer(), nullable=False),
sa.Column('pref_thread_reply', sa.Integer(), nullable=False),
sa.Column('pref_bot', sa.Integer(), nullable=False),
sa.Column('pref_maintainer', sa.Integer(), nullable=False),
sa.Column('pref_editor_alert', sa.Integer(), nullable=False),
sa.Column('pref_editor_misc', sa.Integer(), nullable=False),
sa.Column('pref_other', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('watchers',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('thread_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('user_id', 'thread_id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('watchers')
op.drop_table('user_notification_preferences')
op.drop_table('user_email_verification')
op.drop_table('thread_reply')
op.drop_table('tags')
op.drop_table('provides')
op.drop_table('package_update_config')
op.drop_table('package_review_vote')
op.drop_table('package_release')
op.drop_table('package_game_support')
op.drop_table('package_alias')
op.drop_table('notification')
op.drop_table('maintainers')
op.drop_table('forum_topic')
op.drop_table('dependency')
op.drop_table('content_warnings')
op.drop_table('audit_log_entry')
op.drop_table('api_token')
op.drop_index(op.f('ix_user_username'), table_name='user')
op.drop_table('user')
op.drop_table('thread')
op.drop_table('tag')
op.drop_table('package_screenshot')
op.drop_table('package_review')
op.drop_index('ix_package_search_vector', table_name='package', postgresql_using='gin')
op.drop_table('package')
op.drop_table('meta_package')
op.drop_table('license')
op.drop_table('email_subscription')
op.drop_table('content_warning')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 6dca6eceb04d
Revises: fd25bf3e57c3
Create Date: 2020-01-18 17:32:21.885068
"""
from alembic import op
from sqlalchemy_searchable import sync_trigger
# revision identifiers, used by Alembic.
revision = '6dca6eceb04d'
down_revision = 'fd25bf3e57c3'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
sync_trigger(conn, 'package', 'search_vector', ["name", "title", "short_desc", "desc"])
op.create_check_constraint("name_valid", "package", "name ~* '^[a-z0-9_]+$'")
def downgrade():
conn = op.get_bind()
sync_trigger(conn, 'package', 'search_vector', ["title", "short_desc", "desc"])
op.drop_constraint("name_valid", "package", type_="check")

View File

@ -1,24 +0,0 @@
"""empty message
Revision ID: 6e57b2b4dcdf
Revises: 17b303f33f68
Create Date: 2022-01-22 20:35:25.494712
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6e57b2b4dcdf'
down_revision = '17b303f33f68'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('user', sa.Column('locale', sa.String(length=10), nullable=True))
def downgrade():
op.drop_column('user', 'locale')

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 725ff70ea316
Revises: 51be0401bb85
Create Date: 2021-07-31 19:10:36.683434
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '725ff70ea316'
down_revision = '51be0401bb85'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('license', sa.Column('url', sa.String(length=128), nullable=True, default=None))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('license', 'url')
# ### end Alembic commands ###

View File

@ -1,23 +0,0 @@
"""empty message
Revision ID: 7a48dbd05780
Revises: df66c78e6791
Create Date: 2020-01-24 21:52:49.744404
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '7a48dbd05780'
down_revision = 'df66c78e6791'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('user', sa.Column('github_access_token', sa.String(length=50), nullable=True, server_default=None))
def downgrade():
op.drop_column('user', 'github_access_token')

View File

@ -1,40 +0,0 @@
"""empty message
Revision ID: 7def3e843d04
Revises: dce69ad1e4eb
Create Date: 2019-01-28 20:27:33.760232
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '7def3e843d04'
down_revision = 'dce69ad1e4eb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('minetest_release',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.add_column('package_release', sa.Column('max_rel_id', sa.Integer(), nullable=True, server_default=None))
op.add_column('package_release', sa.Column('min_rel_id', sa.Integer(), nullable=True, server_default=None))
op.create_foreign_key(None, 'package_release', 'minetest_release', ['max_rel_id'], ['id'])
op.create_foreign_key(None, 'package_release', 'minetest_release', ['min_rel_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'package_release', type_='foreignkey')
op.drop_constraint(None, 'package_release', type_='foreignkey')
op.drop_column('package_release', 'min_rel_id')
op.drop_column('package_release', 'max_rel_id')
op.drop_table('minetest_release')
# ### end Alembic commands ###

Some files were not shown because too many files have changed in this diff Show More