Compare commits

...

12 Commits

174 changed files with 1881 additions and 4436 deletions

View File

@ -1,24 +1,22 @@
FROM python:3.10
RUN groupadd -g 5123 cdb && \
useradd -r -u 5123 -g cdb cdb
WORKDIR /home/cdb
RUN mkdir /var/cdb
RUN chown -R cdb:cdb /var/cdb
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 gunicorn
RUN pip3 install pycairo PyGObject
COPY utils utils
COPY config.cfg config.cfg
COPY migrations migrations
COPY app app
COPY translations translations
RUN pybabel compile -d translations
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.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, PackageGameSupport
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
NotificationType, PackageUpdateConfig, License, UserRank, PackageGameSupport
# from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates
from app.tasks.appstreamtasks import importFromFlathub
from app.utils import addNotification, get_system_user
from app.utils.image import get_image_size
@ -89,16 +90,20 @@ def reimport_packages():
return redirect(url_for("todo.view_editor"))
@action("Import forum topic list")
def import_topic_list():
task = importTopicList.delay()
# @action("Import forum topic list")
# def import_topic_list():
# task = importTopicList.delay()
# return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
@action("Import appstream from flathub")
def import_from_flathub():
task = importFromFlathub.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics")))
@action("Check all forum accounts")
def check_all_forum_accounts():
task = checkAllForumAccounts.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
# @action("Check all forum accounts")
# def check_all_forum_accounts():
# task = checkAllForumAccounts.delay()
# return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
@action("Import screenshots")
@ -292,13 +297,12 @@ def delete_inactive_users():
@action("Send Video URL notification")
def remind_video_url():
users = User.query.filter(User.maintained_packages.any(
and_(Package.video_url.is_(None), Package.type==PackageType.GAME, Package.state==PackageState.APPROVED)))
and_(Package.video_url.is_(None), Package.state==PackageState.APPROVED)))
system_user = get_system_user()
for user in users:
packages = db.session.query(Package.title).filter(
or_(Package.author==user, Package.maintainers.any(User.id==user.id)),
Package.video_url.is_(None),
Package.type == PackageType.GAME,
Package.state == PackageState.APPROVED) \
.all()

View File

@ -22,42 +22,42 @@ from wtforms.validators import InputRequired, Length
from app.utils import rank_required
from . import bp
from ...models import UserRank, MinetestRelease, db
from ...models import UserRank, db
@bp.route("/versions/")
@rank_required(UserRank.MODERATOR)
def version_list():
return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
# @bp.route("/versions/")
# @rank_required(UserRank.MODERATOR)
# def version_list():
# return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
class VersionForm(FlaskForm):
name = StringField("Name", [InputRequired(), Length(3, 100)])
protocol = IntegerField("Protocol")
submit = SubmitField("Save")
# class VersionForm(FlaskForm):
# name = StringField("Name", [InputRequired(), Length(3, 100)])
# protocol = IntegerField("Protocol")
# submit = SubmitField("Save")
@bp.route("/versions/new/", methods=["GET", "POST"])
@bp.route("/versions/<name>/edit/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def create_edit_version(name=None):
version = None
if name is not None:
version = MinetestRelease.query.filter_by(name=name).first()
if version is None:
abort(404)
# @bp.route("/versions/new/", methods=["GET", "POST"])
# @bp.route("/versions/<name>/edit/", methods=["GET", "POST"])
# @rank_required(UserRank.MODERATOR)
# def create_edit_version(name=None):
# version = None
# if name is not None:
# version = MinetestRelease.query.filter_by(name=name).first()
# if version is None:
# abort(404)
form = VersionForm(formdata=request.form, obj=version)
if form.validate_on_submit():
if version is None:
version = MinetestRelease(form.name.data)
db.session.add(version)
flash("Created version " + form.name.data, "success")
else:
flash("Updated version " + form.name.data, "success")
# form = VersionForm(formdata=request.form, obj=version)
# if form.validate_on_submit():
# if version is None:
# version = MinetestRelease(form.name.data)
# db.session.add(version)
# flash("Created version " + form.name.data, "success")
# else:
# flash("Updated version " + form.name.data, "success")
form.populate_obj(version)
db.session.commit()
return redirect(url_for("admin.version_list"))
# form.populate_obj(version)
# db.session.commit()
# 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.markdown import render_markdown
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, ForumTopic, \
MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread
from app.models import Tag, PackageState, Package, db, PackageRelease, Permission, ForumTopic, \
APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread
from app.querybuilder import QueryBuilder
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes, get_toplevel_tags
from . import bp
from .auth import is_api_authd
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
@ -89,9 +89,6 @@ def resolve_package_deps(out, package, only_hard, depth=1):
ret = []
out[id] = ret
if package.type != PackageType.MOD:
return
for dep in package.dependencies:
if only_hard and dep.optional:
continue
@ -106,7 +103,7 @@ def resolve_package_deps(out, package, only_hard, depth=1):
fulfilled_by = [ pkg.getId() for pkg in dep.meta_package.packages]
if depth == 1 and not dep.optional:
most_likely = next((pkg for pkg in dep.meta_package.packages if pkg.type == PackageType.MOD), None)
most_likely = next((pkg for pkg in dep.meta_package.packages), None)
if most_likely:
resolve_package_deps(out, most_likely, only_hard, depth + 1)
@ -470,9 +467,11 @@ def homepage():
featured = query.filter(Package.tags.any(name="featured")).order_by(
func.random()).limit(6).all()
new = query.order_by(db.desc(Package.approved_at)).limit(4).all()
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(8).all()
pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(8).all()
toplevel_tags = get_toplevel_tags()
popular = {}
# new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
for toplevel_tag in toplevel_tags:
popular[toplevel_tag.name] = query.filter(Package.tags.any(name=toplevel_tag.name)).order_by(db.desc(Package.score)).limit(8).all()
high_reviewed = query.order_by(db.desc(Package.score - Package.score_downloads)) \
.filter(Package.reviews.any()).limit(4).all()
@ -487,16 +486,15 @@ def homepage():
def mapPackages(packages: List[Package]):
return [pkg.getAsDictionaryShort(current_app.config["BASE_URL"]) for pkg in packages]
popular = { k:mapPackages(v) for (k, v) in popular.items() }
return jsonify({
"count": count,
"downloads": downloads,
"featured": mapPackages(featured),
"new": mapPackages(new),
"updated": mapPackages(updated),
"pop_mod": mapPackages(pop_mod),
"pop_txp": mapPackages(pop_txp),
"pop_game": mapPackages(pop_gam),
"popular": popular,
"toplevel": [ x.getAsDictionary() for x in toplevel_tags ],
"high_reviewed": mapPackages(high_reviewed)
})
@ -505,7 +503,7 @@ def homepage():
@cors_allowed
def welcome_v1():
featured = Package.query \
.filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED,
.filter(Package.state == PackageState.APPROVED,
Package.tags.any(name="featured")) \
.order_by(func.random()) \
.limit(5).all()
@ -520,23 +518,6 @@ def welcome_v1():
"featured": map_packages(featured),
})
@bp.route("/api/minetest_versions/")
@cors_allowed
def versions():
protocol_version = request.args.get("protocol_version")
engine_version = request.args.get("engine_version")
if protocol_version or engine_version:
rel = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version))
if rel is None:
error(404, "No releases found")
return jsonify(rel.getAsDictionary())
return jsonify([rel.getAsDictionary() \
for rel in MinetestRelease.query.all() if rel.getActual() is not None])
@bp.route("/api/dependencies/")
@cors_allowed
def all_deps():
@ -545,7 +526,7 @@ def all_deps():
def format_pkg(pkg: Package):
return {
"type": pkg.type.toName(),
# "type": pkg.type.toName(),
"author": pkg.author.username,
"name": pkg.name,
"provides": [x.name for x in pkg.provides],

View File

@ -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.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.models import APIToken, Package, MinetestRelease, PackageScreenshot
from app.models import APIToken, Package, PackageScreenshot
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,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API"):
min_v = None, max_v = None, reason="API"):
if not token.canOperateOnPackage(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,
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):
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__)
from app.models import *
from app.utils import get_toplevel_tags
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import func
@ -18,11 +19,12 @@ def home():
count = query.count()
featured = query.filter(Package.tags.any(name="featured")).order_by(func.random()).limit(6).all()
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
popular = {}
new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
pop_mod = join(query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score))).limit(8).all()
pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all()
pop_txp = join(query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score))).limit(8).all()
for toplevel_tag in toplevel_tags:
popular[toplevel_tag.name] = join(query.filter(Package.tags.any(name=toplevel_tag.name)).order_by(db.desc(Package.score))).limit(8).all()
high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \
.filter(Package.reviews.any()).limit(4).all()
@ -41,4 +43,4 @@ def home():
.select_from(Tag).outerjoin(Tags).group_by(Tag.id).order_by(db.asc(Tag.title)).all()
return render_template("index.html", count=count, downloads=downloads, tags=tags, featured=featured,
new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam, high_reviewed=high_reviewed, reviews=reviews)
new=new, updated=updated, popular=popular, toplevel=toplevel_tags, high_reviewed=high_reviewed, reviews=reviews)

View File

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

View File

@ -99,10 +99,12 @@ def list_all():
selected_tags = set(qb.tags)
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
return render_template("packages/list.html",
query_hint=title, packages=query.items, pagination=query,
query=search, tags=tags, selected_tags=selected_tags, type=type_name,
authors=authors, packages_count=query.total, topics=topics)
authors=authors, packages_count=query.total, topics=topics, toplevel=toplevel_tags)
def getReleases(package):
@ -120,30 +122,28 @@ def view(package):
package.checkPerm(current_user, Permission.APPROVE_NEW))
conflicting_modnames = None
if show_similar and package.type != PackageType.TXP:
if show_similar:
conflicting_modnames = db.session.query(MetaPackage.name) \
.filter(MetaPackage.id.in_([ mp.id for mp in package.provides ])) \
.filter(MetaPackage.packages.any(Package.id != package.id)) \
.all()
conflicting_modnames += db.session.query(ForumTopic.name) \
.filter(ForumTopic.name.in_([ mp.name for mp in package.provides ])) \
.filter(ForumTopic.topic_id != package.forums) \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
# conflicting_modnames += db.session.query(ForumTopic.name) \
# .filter(ForumTopic.name.in_([ mp.name for mp in package.provides ])) \
# .filter(ForumTopic.topic_id != package.forums) \
# .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
# .order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
# .all()
conflicting_modnames = set([x[0] for x in conflicting_modnames])
packages_uses = None
if package.type == PackageType.MOD:
packages_uses = Package.query.filter(
Package.type == PackageType.MOD,
Package.id != package.id,
Package.state == PackageState.APPROVED,
Package.dependencies.any(
Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
.order_by(db.desc(Package.score)).limit(6).all()
packages_uses = Package.query.filter(
Package.id != package.id,
Package.state == PackageState.APPROVED,
Package.dependencies.any(
Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
.order_by(db.desc(Package.score)).limit(6).all()
releases = getReleases(package)
@ -153,21 +153,21 @@ def view(package):
topic_error = None
topic_error_lvl = "warning"
if package.state != PackageState.APPROVED and package.forums is not None:
errors = []
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>")
topic_error_lvl = "danger"
# if package.state != PackageState.APPROVED and package.forums is not None:
# errors = []
# 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>")
# topic_error_lvl = "danger"
topic = ForumTopic.query.get(package.forums)
if topic is not None:
if topic.author != package.author:
errors.append("<b>" + gettext("Error: Forum topic author doesn't match package author.") + "</b>")
topic_error_lvl = "danger"
elif package.type != PackageType.TXP:
errors.append(gettext("Warning: Forum topic not found. This may happen if the topic has only just been created."))
# topic = ForumTopic.query.get(package.forums)
# if topic is not None:
# if topic.author != package.author:
# errors.append("<b>" + gettext("Error: Forum topic author doesn't match package author.") + "</b>")
# topic_error_lvl = "danger"
# elif package.type != PackageType.ASSETPACK:
# 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)
@ -178,11 +178,13 @@ def view(package):
has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0
toplevel_tags = get_toplevel_tags() #Tag.query.filter_by(is_toplevel=True).all()
return render_template("packages/view.html",
package=package, releases=releases, packages_uses=packages_uses,
conflicting_modnames=conflicting_modnames,
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl,
threads=threads.all(), has_review=has_review)
threads=threads.all(), has_review=has_review, toplevel=toplevel_tags)
@bp.route("/packages/<author>/<name>/shields/<type>/")
@ -226,9 +228,16 @@ def makeLabel(obj):
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)])
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)])
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])
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])
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])
submit = SubmitField(lazy_gettext("Save"))
@ -286,16 +295,13 @@ def create_edit(author=None, name=None):
form.name.data = request.args.get("bname")
form.title.data = request.args.get("title")
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.media_license.data = None
else:
form.tags.data = package.tags
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():
wasNew = False
if not package:
@ -327,7 +333,7 @@ def create_edit(author=None, name=None):
"repo": form.repo.data,
"website": form.website.data,
"issueTracker": form.issueTracker.data,
"forums": form.forums.data,
# "forums": form.forums.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,
packages=package_query.all(),
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all(),
tabs=get_package_tabs(current_user, package), current_tab="edit")
tabs=get_package_tabs(current_user, package), current_tab="edit", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/state/", methods=["POST"])
@ -408,7 +414,7 @@ def move_to_state(package):
def remove(package):
if request.method == "GET":
return render_template("packages/remove.html", package=package,
tabs=get_package_tabs(current_user, package), current_tab="remove")
tabs=get_package_tabs(current_user, package), current_tab="remove", toplevel=get_toplevel_tags())
reason = request.form.get("reason") or "?"
@ -501,7 +507,7 @@ def edit_maintainers(package):
users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).order_by(db.asc(User.username)).all()
return render_template("packages/edit_maintainers.html", package=package, form=form,
users=users, tabs=get_package_tabs(current_user, package), current_tab="maintainers")
users=users, tabs=get_package_tabs(current_user, package), current_tab="maintainers", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/remove-self-maintainer/", methods=["POST"])
@ -540,13 +546,13 @@ def audit(package):
pagination = query.paginate(page, num, True)
return render_template("packages/audit.html", log=pagination.items, pagination=pagination,
package=package, tabs=get_package_tabs(current_user, package), current_tab="audit")
package=package, tabs=get_package_tabs(current_user, package), current_tab="audit", toplevel=get_toplevel_tags())
class PackageAliasForm(FlaskForm):
author = StringField(lazy_gettext("Author Name"), [InputRequired(), Length(1, 50)])
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"))
@ -554,7 +560,7 @@ class PackageAliasForm(FlaskForm):
@rank_required(UserRank.EDITOR)
@is_package_page
def alias_list(package: Package):
return render_template("packages/alias_list.html", package=package)
return render_template("packages/alias_list.html", package=package, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/aliases/new/", methods=["GET", "POST"])
@ -580,7 +586,7 @@ def alias_create_edit(package: Package, alias_id: int = None):
return redirect(package.getURL("packages.alias_list"))
return render_template("packages/alias_create_edit.html", package=package, form=form)
return render_template("packages/alias_create_edit.html", package=package, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/share/")
@ -588,7 +594,7 @@ def alias_create_edit(package: Package, alias_id: int = None):
@is_package_page
def share(package):
return render_template("packages/share.html", package=package,
tabs=get_package_tabs(current_user, package), current_tab="share")
tabs=get_package_tabs(current_user, package), current_tab="share", toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/similar/")
@ -602,12 +608,12 @@ def similar(package):
.order_by(db.desc(Package.score)) \
.all()
similar_topics = ForumTopic.query \
.filter_by(name=package.name) \
.filter(ForumTopic.topic_id != package.forums) \
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
# similar_topics = ForumTopic.query \
# .filter_by(name=package.name) \
# .filter(ForumTopic.topic_id != package.forums) \
# .filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \
# .order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
# .all()
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):
return render_template("packages/releases_list.html",
package=package,
tabs=get_package_tabs(current_user, package), current_tab="releases")
tabs=get_package_tabs(current_user, package), current_tab="releases", toplevel=get_toplevel_tags())
def get_mt_releases(is_max):
query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
if is_max:
query = query.limit(query.count() - 1)
else:
query = query.filter(MinetestRelease.name != "0.4.17")
# def get_mt_releases(is_max):
# query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
# if is_max:
# query = query.limit(query.count() - 1)
# else:
# query = query.filter(MinetestRelease.name != "0.4.17")
return query
# return query
class CreatePackageReleaseForm(FlaskForm):
@ -53,10 +53,6 @@ class CreatePackageReleaseForm(FlaskForm):
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)
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"))
@ -65,10 +61,6 @@ class EditPackageReleaseForm(FlaskForm):
url = StringField(lazy_gettext("URL"), [Optional()])
task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None])
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"))
@ -93,16 +85,14 @@ def create_release(package):
if form.validate_on_submit():
try:
if form["uploadOpt"].data == "vcs":
rel = do_create_vcs_release(current_user, package, form.title.data,
form.vcsLabel.data, form.min_rel.data.getActual(), form.max_rel.data.getActual())
rel = do_create_vcs_release(current_user, package, form.title.data, form.vcsLabel.data)
else:
rel = do_create_zip_release(current_user, package, form.title.data,
form.fileUpload.data, form.min_rel.data.getActual(), form.max_rel.data.getActual())
rel = do_create_zip_release(current_user, package, form.title.data, form.fileUpload.data)
return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL()))
except LogicError as e:
flash(e.message, "danger")
return render_template("packages/release_new.html", package=package, form=form)
return render_template("packages/release_new.html", package=package, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/releases/<id>/download/")
@ -158,8 +148,6 @@ def edit_release(package, id):
if form.validate_on_submit():
if canEdit:
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):
release.url = form["url"].data
@ -175,19 +163,19 @@ def edit_release(package, id):
db.session.commit()
return redirect(package.getURL("packages.list_releases"))
return render_template("packages/release_edit.html", package=package, release=release, form=form)
return render_template("packages/release_edit.html", package=package, release=release, form=form, toplevel=get_toplevel_tags())
class BulkReleaseForm(FlaskForm):
set_min = BooleanField(lazy_gettext("Set Min"))
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)
set_max = BooleanField(lazy_gettext("Set Max"))
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)
only_change_none = BooleanField(lazy_gettext("Only change values previously set as none"))
submit = SubmitField(lazy_gettext("Update"))
# class BulkReleaseForm(FlaskForm):
# set_min = BooleanField(lazy_gettext("Set Min"))
# 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)
# set_max = BooleanField(lazy_gettext("Set Max"))
# 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)
# only_change_none = BooleanField(lazy_gettext("Only change values previously set as none"))
# submit = SubmitField(lazy_gettext("Update"))
@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():
only_change_none = form.only_change_none.data
for release in package.releases.all():
if form["set_min"].data and (not only_change_none or release.min_rel is None):
release.min_rel = form["min_rel"].data.getActual()
if form["set_max"].data and (not only_change_none or release.max_rel is None):
release.max_rel = form["max_rel"].data.getActual()
# for release in package.releases.all():
# if form["set_min"].data and (not only_change_none or release.min_rel is None):
# release.min_rel = form["min_rel"].data.getActual()
# if form["set_max"].data and (not only_change_none or release.max_rel is None):
# release.max_rel = form["max_rel"].data.getActual()
db.session.commit()
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"])
@ -313,7 +301,7 @@ def update_config(package):
return redirect(package.getURL("packages.list_releases"))
return render_template("packages/update_config.html", package=package, form=form)
return render_template("packages/update_config.html", package=package, form=form, toplevel=get_toplevel_tags())
@bp.route("/packages/<author>/<name>/setup-releases/")
@ -326,7 +314,7 @@ def setup_releases(package):
if package.update_config:
return redirect(package.getURL("packages.update_config"))
return render_template("packages/release_wizard.html", package=package)
return render_template("packages/release_wizard.html", package=package, toplevel=get_toplevel_tags())
@bp.route("/user/update-configs/")
@ -355,4 +343,4 @@ def bulk_update_config(username=None):
Package.update_config.has()) \
.order_by(db.asc(Package.title)).all()
return render_template("packages/bulk_update_conf.html", user=user, confs=confs, form=form)
return render_template("packages/bulk_update_conf.html", user=user, confs=confs, form=form, toplevel=get_toplevel_tags())

View File

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

View File

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

View File

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

View File

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

View File

@ -1,37 +1,72 @@
import string
import random
from .models import *
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):
admin_user = User("rubenwardy")
admin_user = User("libregaming")
admin_user.is_active = True
admin_user.password = make_flask_login_password("tuckfrump")
admin_user.github_username = "rubenwardy"
admin_user.forums_username = "rubenwardy"
password = generate_password()
admin_user.password = make_flask_login_password(password)
admin_user.github_username = "libregaming"
admin_user.forums_username = "libregaming"
admin_user.rank = UserRank.ADMIN
session.add(admin_user)
print("#####################################")
print("Admin user : libregaming")
print("Admin password: " + password)
print("#####################################")
system_user = User("ContentDB", active=False)
system_user.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
system_user.rank = UserRank.BOT
session.add(system_user)
session.add(MinetestRelease("None", 0))
session.add(MinetestRelease("0.4.16/17", 32))
session.add(MinetestRelease("5.0", 37))
session.add(MinetestRelease("5.1", 38))
session.add(MinetestRelease("5.2", 39))
session.add(MinetestRelease("5.3", 39))
appstream_user = User("AppStreamBot", active=False)
appstream_user.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
appstream_user.rank = UserRank.BOT
session.add(appstream_user)
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 = {}
for tag in ["Inventory", "Mapgen", "Building",
"Mobs and NPCs", "Tools", "Player effects",
"Environment", "Transport", "Maintenance", "Plants and farming",
"PvP", "PvE", "Survival", "Creative", "Puzzle", "Multiplayer", "Singleplayer", "Featured"]:
for tag in [
"Action",
"Adventure",
"Arcade",
"Board",
"Blocks",
"Card",
"Kids",
"Logic",
"RolePlaying",
"Shooter",
"Simulation",
"Sports",
"Strategy"
]:
row = Tag(tag)
tags[row.name] = row
session.add(row)
tags["featured"] = featured
licenses = {}
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
"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() }
tags = { x.name : x for x in Tag.query.all() }
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
v4 = MinetestRelease.query.filter_by(protocol=32).first()
v50 = MinetestRelease.query.filter_by(protocol=37).first()
v51 = MinetestRelease.query.filter_by(protocol=38).first()
# v4 = MinetestRelease.query.filter_by(protocol=32).first()
# v50 = MinetestRelease.query.filter_by(protocol=37).first()
# v51 = MinetestRelease.query.filter_by(protocol=38).first()
ez = User("Shara")
ez.github_username = "Ezhh"
@ -60,7 +95,7 @@ def populate_test_data(session):
ez.rank = UserRank.EDITOR
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)
jeija = User("Jeija")
@ -69,25 +104,25 @@ def populate_test_data(session):
session.add(jeija)
mod = Package()
mod.state = PackageState.APPROVED
mod.name = "alpha"
mod.title = "Alpha Test"
mod.license = licenses["MIT"]
mod.media_license = licenses["MIT"]
mod.type = PackageType.MOD
mod.author = admin_user
mod.tags.append(tags["mapgen"])
mod.tags.append(tags["environment"])
mod.repo = "https://github.com/ezhh/other_worlds"
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
mod.forums = 16015
mod.short_desc = "The content library should not be used yet as it is still in alpha"
mod.desc = "This is the long desc"
session.add(mod)
tool = Package()
tool.state = PackageState.APPROVED
tool.name = "alpha"
tool.title = "Alpha Test"
tool.license = licenses["MIT"]
tool.media_license = licenses["MIT"]
# tool.type = PackageType.TOOL
tool.author = admin_user
tool.tags.append(tags["mapgen"])
tool.tags.append(tags["environment"])
tool.repo = "https://github.com/ezhh/other_worlds"
tool.issueTracker = "https://github.com/ezhh/other_worlds/issues"
tool.forums = 16015
tool.short_desc = "The content library should not be used yet as it is still in alpha"
tool.desc = "This is the long desc"
session.add(tool)
rel = PackageRelease()
rel.package = mod
rel.package = tool
rel.title = "v1.0.0"
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
rel.approved = True
@ -99,11 +134,11 @@ def populate_test_data(session):
mod1.title = "Awards"
mod1.license = licenses["LGPLv2.1"]
mod1.media_license = licenses["MIT"]
mod1.type = PackageType.MOD
# mod1.type = PackageType.TOOL
mod1.author = admin_user
mod1.tags.append(tags["player_effects"])
mod1.repo = "https://github.com/rubenwardy/awards"
mod1.issueTracker = "https://github.com/rubenwardy/awards/issues"
mod1.repo = "https://github.com/libregaming/awards"
mod1.issueTracker = "https://github.com/libregaming/awards/issues"
mod1.forums = 4870
mod1.short_desc = "Adds achievements and an API to register new ones."
mod1.desc = """
@ -126,7 +161,7 @@ awards.register_achievement("award_mesefind",{
rel.package = mod1
rel.min_rel = v51
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
session.add(rel)
@ -135,7 +170,7 @@ awards.register_achievement("award_mesefind",{
mod2.name = "mesecons"
mod2.title = "Mesecons"
mod2.tags.append(tags["tools"])
mod2.type = PackageType.MOD
# mod2.type = PackageType.TOOL
mod2.license = licenses["LGPLv3"]
mod2.media_license = licenses["MIT"]
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 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.
@ -160,14 +195,14 @@ Go get it!
[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.
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.
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!
@ -219,97 +254,97 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod1)
session.add(mod2)
mod = Package()
mod.state = PackageState.APPROVED
mod.name = "handholds"
mod.title = "Handholds"
mod.license = licenses["MIT"]
mod.media_license = licenses["MIT"]
mod.type = PackageType.MOD
mod.author = ez
mod.tags.append(tags["player_effects"])
mod.repo = "https://github.com/ezhh/handholds"
mod.issueTracker = "https://github.com/ezhh/handholds/issues"
mod.forums = 17069
mod.short_desc = "Adds hand holds and climbing thingies"
mod.desc = "This is the long desc"
session.add(mod)
tool = Package()
tool.state = PackageState.APPROVED
tool.name = "handholds"
tool.title = "Handholds"
tool.license = licenses["MIT"]
tool.media_license = licenses["MIT"]
# tool.type = PackageType.TOOL
tool.author = ez
tool.tags.append(tags["player_effects"])
tool.repo = "https://github.com/ezhh/handholds"
tool.issueTracker = "https://github.com/ezhh/handholds/issues"
tool.forums = 17069
tool.short_desc = "Adds hand holds and climbing thingies"
tool.desc = "This is the long desc"
session.add(tool)
rel = PackageRelease()
rel.package = mod
rel.package = tool
rel.title = "v1.0.0"
rel.max_rel = v4
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
rel.approved = True
session.add(rel)
mod = Package()
mod.state = PackageState.APPROVED
mod.name = "other_worlds"
mod.title = "Other Worlds"
mod.license = licenses["MIT"]
mod.media_license = licenses["MIT"]
mod.type = PackageType.MOD
mod.author = ez
mod.tags.append(tags["mapgen"])
mod.tags.append(tags["environment"])
mod.repo = "https://github.com/ezhh/other_worlds"
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
mod.forums = 16015
mod.short_desc = "Adds space with asteroids and comets"
mod.desc = "This is the long desc"
session.add(mod)
tool = Package()
tool.state = PackageState.APPROVED
tool.name = "other_worlds"
tool.title = "Other Worlds"
tool.license = licenses["MIT"]
tool.media_license = licenses["MIT"]
# tool.type = PackageType.TOOL
tool.author = ez
tool.tags.append(tags["mapgen"])
tool.tags.append(tags["environment"])
tool.repo = "https://github.com/ezhh/other_worlds"
tool.issueTracker = "https://github.com/ezhh/other_worlds/issues"
tool.forums = 16015
tool.short_desc = "Adds space with asteroids and comets"
tool.desc = "This is the long desc"
session.add(tool)
mod = Package()
mod.state = PackageState.APPROVED
mod.name = "food"
mod.title = "Food"
mod.license = licenses["LGPLv2.1"]
mod.media_license = licenses["MIT"]
mod.type = PackageType.MOD
mod.author = admin_user
mod.tags.append(tags["player_effects"])
mod.repo = "https://github.com/rubenwardy/food/"
mod.issueTracker = "https://github.com/rubenwardy/food/issues/"
mod.forums = 2960
mod.short_desc = "Adds lots of food and an API to manage ingredients"
mod.desc = "This is the long desc"
session.add(mod)
tool = Package()
tool.state = PackageState.APPROVED
tool.name = "food"
tool.title = "Food"
tool.license = licenses["LGPLv2.1"]
tool.media_license = licenses["MIT"]
# tool.type = PackageType.TOOL
tool.author = admin_user
tool.tags.append(tags["player_effects"])
tool.repo = "https://github.com/libregaming/food/"
tool.issueTracker = "https://github.com/libregaming/food/issues/"
tool.forums = 2960
tool.short_desc = "Adds lots of food and an API to manage ingredients"
tool.desc = "This is the long desc"
session.add(tool)
mod = Package()
mod.state = PackageState.APPROVED
mod.name = "food_sweet"
mod.title = "Sweet Foods"
mod.license = licenses["CC0"]
mod.media_license = licenses["MIT"]
mod.type = PackageType.MOD
mod.author = admin_user
mod.tags.append(tags["player_effects"])
mod.repo = "https://github.com/rubenwardy/food_sweet/"
mod.issueTracker = "https://github.com/rubenwardy/food_sweet/issues/"
mod.forums = 9039
mod.short_desc = "Adds sweet food"
mod.desc = "This is the long desc"
food_sweet = mod
session.add(mod)
tool = Package()
tool.state = PackageState.APPROVED
tool.name = "food_sweet"
tool.title = "Sweet Foods"
tool.license = licenses["CC0"]
tool.media_license = licenses["MIT"]
# tool.type = PackageType.TOOL
tool.author = admin_user
tool.tags.append(tags["player_effects"])
tool.repo = "https://github.com/libregaming/food_sweet/"
tool.issueTracker = "https://github.com/libregaming/food_sweet/issues/"
tool.forums = 9039
tool.short_desc = "Adds sweet food"
tool.desc = "This is the long desc"
food_sweet = tool
session.add(tool)
game1 = Package()
game1.state = PackageState.APPROVED
game1.name = "capturetheflag"
game1.title = "Capture The Flag"
game1.type = PackageType.GAME
# game1.type = PackageType.GAME
game1.license = licenses["LGPLv2.1"]
game1.media_license = licenses["MIT"]
game1.author = admin_user
game1.tags.append(tags["pvp"])
game1.tags.append(tags["survival"])
game1.tags.append(tags["multiplayer"])
game1.repo = "https://github.com/rubenwardy/capturetheflag"
game1.issueTracker = "https://github.com/rubenwardy/capturetheflag/issues"
game1.repo = "https://github.com/libregaming/capturetheflag"
game1.issueTracker = "https://github.com/libregaming/capturetheflag/issues"
game1.forums = 12835
game1.short_desc = "Two teams battle to snatch and return the enemy's flag, before the enemy takes their own!"
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)//'>`
@ -351,26 +386,26 @@ Uses the CTF PvP Engine.
rel = PackageRelease()
rel.package = game1
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
session.add(rel)
mod = Package()
mod.state = PackageState.APPROVED
mod.name = "pixelbox"
mod.title = "PixelBOX Reloaded"
mod.license = licenses["CC0"]
mod.media_license = licenses["MIT"]
mod.type = PackageType.TXP
mod.author = admin_user
mod.forums = 14132
mod.short_desc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit"
mod.desc = "This is the long desc"
session.add(mod)
tool = Package()
tool.state = PackageState.APPROVED
tool.name = "pixelbox"
tool.title = "PixelBOX Reloaded"
tool.license = licenses["CC0"]
tool.media_license = licenses["MIT"]
# tool.type = PackageType.ASSETPACK
tool.author = admin_user
tool.forums = 14132
tool.short_desc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit"
tool.desc = "This is the long desc"
session.add(tool)
rel = PackageRelease()
rel.package = mod
rel.package = tool
rel.title = "v1.0.0"
rel.url = "http://mamadou3.free.fr/Minetest/PixelBOX.zip"
rel.approved = True
@ -378,16 +413,16 @@ Uses the CTF PvP Engine.
session.commit()
metas = {}
for package in Package.query.filter_by(type=PackageType.MOD).all():
meta = None
try:
meta = metas[package.name]
except KeyError:
meta = MetaPackage(package.name)
session.add(meta)
metas[package.name] = meta
package.provides.append(meta)
# metas = {}
# for package in Package.query.filter_by(type=PackageType.TOOL).all():
# meta = None
# try:
# meta = metas[package.name]
# except KeyError:
# meta = MetaPackage(package.name)
# session.add(meta)
# metas[package.name] = meta
# package.provides.append(meta)
dep = Dependency(food_sweet, meta=metas["food"])
session.add(dep)
# dep = Dependency(food_sweet, meta=metas["food"])
# session.add(dep)

View File

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

View File

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

View File

@ -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: Have a high resolution cover image on ContentDB (at least 1280x720 pixels).
It may be shown cropped to 16:9 aspect ratio, or shorter.
* MUST: mod.conf/game.conf/texture_pack.conf present with:
* name (if mod or game)
* MUST: tool.conf/game.conf/texture_pack.conf present with:
* name (if tool or game)
* description
* dependencies (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.**
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
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

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:
* `mod.conf` for mods.
* `modpack.conf` for mod packs.
* `tool.conf` for mods.
* `modpack.conf` for tool packs.
* `game.conf` for games.
* `texture_pack.conf` for texture packs.
@ -36,7 +36,7 @@ ContentDB understands the following information:
and for mods only:
* `name` - the mod technical name.
* `name` - the tool technical name.
## .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:
* `type`: One of `GAME`, `MOD`, `TXP`.
* `type`: One of `GAME`, `TOOL`, `ASSETPACK`.
* `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved).
* `short_description`

View File

@ -3,7 +3,7 @@ title: Top Packages Algorithm
## Package Score
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.
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>
* **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>
* **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**,
@ -51,14 +51,14 @@ as this will help advise players.
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
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.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
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.
### 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
mod if it's a fork of that mod (or a close reimplementation). In real terms, it
should be possible to use the new mod as a drop-in replacement.
tool if it's a fork of that tool (or a close reimplementation). In real terms, it
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
reimplementation of the mod that owns the name.
We reserve the right to decide whether a tool counts as a fork or
reimplementation of the tool that owns the name.
## 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.
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
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
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 app.logic.LogicError import LogicError
from app.models import Package, MetaPackage, PackageType, PackageState, PackageGameSupport, db
from app.models import Package, MetaPackage, PackageState, PackageGameSupport, db
"""
get_game_support(package):
@ -128,39 +128,36 @@ class GameSupportResolver:
history = history.copy()
history.append(key)
if package.type == PackageType.GAME:
return PackageSet([package])
# if package.type == PackageType.GAME:
return PackageSet([package])
if key in self.resolved_packages:
return self.resolved_packages.get(key)
# if key in self.resolved_packages:
# return self.resolved_packages.get(key)
if key in self.checked_packages:
print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
return PackageSet()
# if key in self.checked_packages:
# print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
# return PackageSet()
self.checked_packages.add(key)
# self.checked_packages.add(key)
if package.type != PackageType.MOD:
raise LogicError(500, "Got non-mod")
# retval = PackageSet()
retval = PackageSet()
# for dep in package.dependencies.filter_by(optional=False).all():
# ret = self.resolve_for_meta_package(dep.meta_package, history)
# if len(ret) == 0:
# continue
# elif len(retval) == 0:
# retval.update(ret)
# else:
# retval.intersection_update(ret)
# if len(retval) == 0:
# raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games")
for dep in package.dependencies.filter_by(optional=False).all():
ret = self.resolve_for_meta_package(dep.meta_package, history)
if len(ret) == 0:
continue
elif len(retval) == 0:
retval.update(ret)
else:
retval.intersection_update(ret)
if len(retval) == 0:
raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games")
self.resolved_packages[key] = retval
return retval
# self.resolved_packages[key] = retval
# return retval
def update_all(self) -> None:
for package in Package.query.filter(Package.type == PackageType.MOD, Package.state != PackageState.DELETED).all():
for package in Package.query.filter(Package.state != PackageState.DELETED).all():
retval = self.resolve(package, [])
for game in retval:
support = PackageGameSupport(package, game)

View File

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

View File

@ -22,7 +22,7 @@ from flask_babel import lazy_gettext
from app.logic.LogicError import LogicError
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.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,
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)
rel = PackageRelease()
@ -46,8 +46,6 @@ def do_create_vcs_release(user: User, package: Package, title: str, ref: str,
rel.title = title
rel.url = ""
rel.task_id = uuid()
rel.min_rel = min_v
rel.max_rel = max_v
db.session.add(rel)
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,
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None,
min_v = None, max_v = None, reason: str = None,
commit_hash: str = None):
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.task_id = uuid()
rel.commit_hash = commit_hash
rel.min_rel = min_v
rel.max_rel = max_v
db.session.add(rel)
if reason is None:

View File

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

View File

@ -23,7 +23,7 @@ from flask_babel import lazy_gettext
from flask_sqlalchemy import BaseQuery
from sqlalchemy_searchable import SearchQueryMixin
from sqlalchemy_utils.types import TSVectorType
from app.markdown import render_markdown
from . import db
from .users import Permission, UserRank, User
from .. import app
@ -48,49 +48,49 @@ class License(db.Model):
return self.name
class PackageType(enum.Enum):
MOD = "Mod"
GAME = "Game"
TXP = "Texture Pack"
# class PackageType(enum.Enum):
# GAME = "Game"
# TOOL = "Tool"
# ASSETPACK = "Asset Pack"
def toName(self):
return self.name.lower()
# def toName(self):
# return self.name.lower()
def __str__(self):
return self.name
# def __str__(self):
# return self.name
@property
def text(self):
if self == PackageType.MOD:
return lazy_gettext("Mod")
elif self == PackageType.GAME:
return lazy_gettext("Game")
elif self == PackageType.TXP:
return lazy_gettext("Texture Pack")
# @property
# def text(self):
# if self == PackageType.TOOL:
# return lazy_gettext("Tool")
# elif self == PackageType.GAME:
# return lazy_gettext("Game")
# elif self == PackageType.ASSETPACK:
# return lazy_gettext("Asset Pack")
@property
def plural(self):
if self == PackageType.MOD:
return lazy_gettext("Mods")
elif self == PackageType.GAME:
return lazy_gettext("Games")
elif self == PackageType.TXP:
return lazy_gettext("Texture Packs")
# @property
# def plural(self):
# if self == PackageType.TOOL:
# return lazy_gettext("Tools")
# elif self == PackageType.GAME:
# return lazy_gettext("Games")
# elif self == PackageType.ASSETPACK:
# return lazy_gettext("Asset Packs")
@classmethod
def get(cls, name):
try:
return PackageType[name.upper()]
except KeyError:
return None
# @classmethod
# def get(cls, name):
# try:
# return PackageType[name.upper()]
# except KeyError:
# return None
@classmethod
def choices(cls):
return [(choice, choice.text) for choice in cls]
# @classmethod
# def choices(cls):
# return [(choice, choice.text) for choice in cls]
@classmethod
def coerce(cls, item):
return item if type(item) == PackageType else PackageType[item.upper()]
# @classmethod
# def coerce(cls, item):
# return item if type(item) == PackageType else PackageType[item.upper()]
class PackageDevState(enum.Enum):
@ -218,7 +218,7 @@ class PackagePropertyKey(enum.Enum):
title = "Title"
short_desc = "Short Description"
desc = "Description"
type = "Type"
# type = "Type"
license = "License"
media_license = "Media License"
tags = "Tags"
@ -226,7 +226,7 @@ class PackagePropertyKey(enum.Enum):
repo = "Repository"
website = "Website"
issueTracker = "Issue Tracker"
forums = "Forum Topic ID"
# forums = "Forum Topic ID"
def convert(self, value):
if self == PackagePropertyKey.tags:
@ -376,11 +376,13 @@ class Package(db.Model):
title = db.Column(db.Unicode(100), nullable=False)
short_desc = db.Column(db.Unicode(200), nullable=False)
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)
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",
weights={ "name": "A", "title": "B", "short_desc": "C", "desc": "D" }))
@ -532,7 +534,7 @@ class Package(db.Model):
"title": self.title,
"author": self.author.username,
"short_description": short_desc,
"type": self.type.toName(),
# "type": self.type.toName(),
"release": release_id,
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
"aliases": [ alias.getAsDictionary() for alias in self.aliases ],
@ -557,7 +559,7 @@ class Package(db.Model):
"title": self.title,
"short_description": self.short_desc,
"long_description": self.desc,
"type": self.type.toName(),
# "type": self.type.toName(),
"created_at": self.created_at.isoformat(),
"license": self.license.name,
@ -636,9 +638,7 @@ class Package(db.Model):
def getDownloadRelease(self, version=None):
for rel in self.releases:
if rel.approved and (version is None or
((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))):
if rel.approved:
return rel
return None
@ -728,7 +728,7 @@ class Package(db.Model):
return False
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
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 ]
self.score = self.score_downloads + sum(review_scores)
class MetaPackage(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
dependencies = db.relationship("Dependency", back_populates="meta_package", lazy="dynamic")
packages = db.relationship("Package", lazy="dynamic", back_populates="provides", secondary=PackageProvides)
mp_name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'")
mp_name_valid = db.CheckConstraint("name ~* '^[a-zA-Z0-9_\-\.]+$'")
def __init__(self, name=None):
self.name = name
@ -848,6 +847,7 @@ class Tag(db.Model):
textColor = db.Column(db.String(6), nullable=False)
views = db.Column(db.Integer, nullable=False, default=0)
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)
@ -857,7 +857,7 @@ class Tag(db.Model):
self.textColor = textColor
import re
regex = re.compile("[^a-z_]")
regex = re.compile("[^0-9a-z_]")
self.name = regex.sub("", self.title.lower().replace(" ", "_"))
def getAsDictionary(self):
@ -867,52 +867,11 @@ class Tag(db.Model):
"title": self.title,
"description": description,
"is_protected": self.is_protected,
"is_toplevel": self.is_toplevel,
"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):
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)
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)
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])
channel = db.Column(db.String(200), nullable=False, default="")
# 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)")
@ -948,8 +903,7 @@ class PackageRelease(db.Model):
"release_date": self.releaseDate.isoformat(),
"commit": self.commit_hash,
"downloads": self.downloads,
"min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(),
"max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(),
"channel": self.channel,
}
def getLongAsDictionary(self):
@ -960,8 +914,7 @@ class PackageRelease(db.Model):
"release_date": self.releaseDate.isoformat(),
"commit": self.commit_hash,
"downloads": self.downloads,
"min_minetest_version": self.min_rel and self.min_rel.getAsDictionary(),
"max_minetest_version": self.max_rel and self.max_rel.getAsDictionary(),
"channel": self.channel,
"package": self.package.getAsDictionaryKey()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

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 ",
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 ",
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 ",
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});

View File

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

View File

@ -38,7 +38,7 @@ $(function() {
}
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.`;
let hint_thegame = `Tip:
@ -47,8 +47,8 @@ $(function() {
$("#short_desc").on("change paste keyup", function() {
const val = $(this).val().toLowerCase();
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) {
if (val.indexOf("minetest") >= 0 || val.indexOf("tool") >= 0 ||
val.indexOf("modpack") >= 0 || val.indexOf("tool pack") >= 0) {
showHint($(this), hint_mtmods);
} else if (val.indexOf("the game") >= 0) {
showHint($(this), hint_thegame);

View File

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

View File

@ -66,10 +66,10 @@ def make_celery(app):
celery = make_celery(app)
CELERYBEAT_SCHEDULE = {
'topic_list_import': {
'task': 'app.tasks.forumtasks.importTopicList',
'schedule': crontab(minute=1, hour=1), # 0101
},
# 'topic_list_import': {
# 'task': 'app.tasks.forumtasks.importTopicList',
# 'schedule': crontab(minute=1, hour=1), # 0101
# },
'package_score_update': {
'task': 'app.tasks.pkgtasks.updatePackageScores',
'schedule': crontab(minute=10, hour=1), # 0110
@ -78,10 +78,10 @@ CELERYBEAT_SCHEDULE = {
'task': 'app.tasks.importtasks.check_for_updates',
'schedule': crontab(minute=10, hour=1), # 0110
},
'send_pending_notifications': {
'task': 'app.tasks.emails.send_pending_notifications',
'schedule': crontab(minute='*/5'), # every 5 minutes
},
# 'send_pending_notifications': {
# 'task': 'app.tasks.emails.send_pending_notifications',
# 'schedule': crontab(minute='*/5'), # every 5 minutes
# },
'send_notification_digests': {
'task': 'app.tasks.emails.send_pending_digests',
'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_]+)\]")
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):
for tag in reversed(regex_tag.findall(taglist)):
if len(tag) < 30 and not tag in BANNED_NAMES and \
@ -112,81 +112,81 @@ def getLinksFromModSearch():
pass
except urllib.error.URLError:
print("Unable to open krocks mod search!")
print("Unable to open krocks tool search!")
return links
return links
@celery.task()
def importTopicList():
links_by_id = getLinksFromModSearch()
# @celery.task()
# def importTopicList():
# links_by_id = getLinksFromModSearch()
info_by_id = {}
getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': False })
getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': True })
getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False })
getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True })
# info_by_id = {}
# getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': False })
# getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.TOOL, 'wip': True })
# getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False })
# getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True })
# Caches
username_to_user = {}
topics_by_id = {}
for topic in ForumTopic.query.all():
topics_by_id[topic.topic_id] = topic
# # Caches
# username_to_user = {}
# topics_by_id = {}
# for topic in ForumTopic.query.all():
# topics_by_id[topic.topic_id] = topic
def get_or_create_user(username):
user = username_to_user.get(username)
if user:
return user
# def get_or_create_user(username):
# user = username_to_user.get(username)
# if user:
# return user
if not is_username_valid(username):
return None
# if not is_username_valid(username):
# return None
user = User.query.filter_by(forums_username=username).first()
if user is None:
user = User.query.filter_by(username=username).first()
if user:
return None
# user = User.query.filter_by(forums_username=username).first()
# if user is None:
# user = User.query.filter_by(username=username).first()
# if user:
# return None
user = User(username)
user.forums_username = username
db.session.add(user)
# user = User(username)
# user.forums_username = username
# db.session.add(user)
username_to_user[username] = user
return user
# username_to_user[username] = user
# return user
# Create or update
for info in info_by_id.values():
id = int(info["id"])
# # Create or update
# for info in info_by_id.values():
# id = int(info["id"])
# Get author
username = info["author"]
user = get_or_create_user(username)
if user is None:
print("Error! Unable to create user {}".format(username), file=sys.stderr)
continue
# # Get author
# username = info["author"]
# user = get_or_create_user(username)
# if user is None:
# print("Error! Unable to create user {}".format(username), file=sys.stderr)
# continue
# Get / add row
topic = topics_by_id.get(id)
if topic is None:
topic = ForumTopic()
db.session.add(topic)
# # Get / add row
# topic = topics_by_id.get(id)
# if topic is None:
# topic = ForumTopic()
# db.session.add(topic)
# Parse title
title, name = parseTitle(info["title"])
# # Parse title
# title, name = parseTitle(info["title"])
# Get link
link = links_by_id.get(id)
# # Get link
# link = links_by_id.get(id)
# Fill row
topic.topic_id = int(id)
topic.author = user
topic.type = info["type"]
topic.title = title
topic.name = name
topic.link = link
topic.wip = info["wip"]
topic.posts = int(info["posts"])
topic.views = int(info["views"])
topic.created_at = info["date"]
# # Fill row
# topic.topic_id = int(id)
# topic.author = user
# topic.type = info["type"]
# topic.title = title
# topic.name = name
# topic.link = link
# topic.wip = info["wip"]
# topic.posts = int(info["posts"])
# topic.views = int(info["views"])
# topic.created_at = info["date"]
db.session.commit()
# db.session.commit()

View File

@ -75,11 +75,8 @@ def getMeta(urlstr, author):
def postReleaseCheckUpdate(self, release: PackageRelease, path):
try:
tree = build_tree(path, expected_type=ContentType[release.package.type.name],
author=release.package.author.username, name=release.package.name)
tree = build_tree(path, author=release.package.author.username, name=release.package.name)
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 = {}
def getMetaPackages(names):
@ -99,14 +96,9 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
optional_depends = tree.fold("meta", "optional_depends")
# Filter out provides
for mod in provides:
depends.discard(mod)
optional_depends.discard(mod)
# 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)
for tool in provides:
depends.discard(tool)
optional_depends.discard(tool)
# Add dependencies
for meta in getMetaPackages(depends):
@ -116,16 +108,16 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
db.session.add(Dependency(package, meta=meta, optional=True))
# Update game supports
if package.type == PackageType.MOD:
resolver = GameSupportResolver()
resolver.update(package)
# if package.type == PackageType.TOOL:
# resolver = GameSupportResolver()
# resolver.update(package)
# Update min/max
if tree.meta.get("min_minetest_version"):
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
# # Update min/max
# if tree.meta.get("min_minetest_version"):
# release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
if tree.meta.get("max_minetest_version"):
release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None)
# if tree.meta.get("max_minetest_version"):
# release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None)
try:
with open(os.path.join(tree.baseDir, ".cdb.json"), "r") as f:

View File

@ -8,13 +8,13 @@ class MinetestCheckError(Exception):
class ContentType(Enum):
UNKNOWN = "unknown"
MOD = "mod"
TOOL = "tool"
MODPACK = "modpack"
GAME = "game"
TXP = "texture pack"
ASSETPACK = "texture pack"
def isModLike(self):
return self == ContentType.MOD or self == ContentType.MODPACK
return self == ContentType.TOOL or self == ContentType.MODPACK
def validate_same(self, other):
"""
@ -22,18 +22,6 @@ class ContentType(Enum):
"""
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
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"):
return ContentType.GAME
elif os.path.isfile(path + "/init.lua"):
return ContentType.MOD
return ContentType.TOOL
elif os.path.isfile(path + "/modpack.txt") or \
os.path.isfile(path + "/modpack.conf"):
return ContentType.MODPACK
# elif os.path.isdir(path + "/mods"):
# return ContentType.GAME
elif os.path.isfile(path + "/texture_pack.conf"):
return ContentType.TXP
return ContentType.ASSETPACK
else:
return ContentType.UNKNOWN
@ -53,13 +53,7 @@ class PackageTreeNode:
self.read_meta()
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")
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:
self.add_children_from_mod_dir(None)
@ -72,11 +66,11 @@ class PackageTreeNode:
def getMetaFileName(self):
if self.type == ContentType.GAME:
return "game.conf"
elif self.type == ContentType.MOD:
return "mod.conf"
elif self.type == ContentType.TOOL:
return "tool.conf"
elif self.type == ContentType.MODPACK:
return "modpack.conf"
elif self.type == ContentType.TXP:
elif self.type == ContentType.ASSETPACK:
return "texture_pack.conf"
else:
return None
@ -94,14 +88,7 @@ class PackageTreeNode:
conf = parse_conf(myfile.read())
for key, value in conf.items():
result[key] = value
except SyntaxError as e:
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))
except IOError: pass
# description.txt
if not "description" in result:
@ -139,16 +126,7 @@ class PackageTreeNode:
result["optional_depends"] = []
def checkDependencies(deps):
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))
def checkDependencies(deps): pass
# Check dependencies
checkDependencies(result["depends"])
@ -187,17 +165,11 @@ class PackageTreeNode:
path = os.path.join(dir, entry)
if not entry.startswith('.') and os.path.isdir(path):
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)
def getModNames(self):
return self.fold("name", type=ContentType.MOD)
return self.fold("name", type=ContentType.TOOL)
# attr: Attribute name
# 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('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.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.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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,23 +14,8 @@
<h3>{{ _("Games") }}</h3>
{{ render_pkggrid(mpackage.packages.filter_by(type="GAME", state="APPROVED").all()) }}
<h3>{{ _("Mods") }}</h3>
{{ render_pkggrid(mpackage.packages.filter_by(type="MOD", 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 %}
<h3>{{ _("Tools") }}</h3>
{{ render_pkggrid(mpackage.packages.filter_by(type="TOOL", state="APPROVED").all()) }}
<h2>{{ _("Required By") }}</h2>
{{ render_pkggrid(dependers) }}

View File

@ -46,7 +46,7 @@
<div class="alert alert-secondary">
<a class="float-right btn btn-sm btn-default" href="/help/package_config/#cdbjson">{{ _("Read more") }}</a>
{{ _("You can include a .cdb.json file in your %(type)s to update these details automatically.", type=package.type.text.lower()) }}
{{ _("You can include a .cdb.json file in your projects to update these details automatically.") }}
</div>
{% endif %}
@ -72,7 +72,7 @@
{{ render_field(form.name, class_="pkg_meta col-sm-4",
readonly=True, hint=_("Please open a thread to request a name change")) }}
{% 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 %}
</div>
{{ render_field(form.short_desc, class_="pkg_meta") }}
@ -113,10 +113,6 @@
{{ render_field(form.website, 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.")) }}
</fieldset>

View File

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

View File

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

View File

@ -43,25 +43,6 @@
{% 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">
{{ render_submit_field(form.submit) }}
</p>

View File

@ -58,30 +58,6 @@
tips on customising releases.") }}
</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">
{{ render_submit_field(form.submit) }}
</p>

View File

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

View File

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

View File

@ -28,18 +28,4 @@
{% endfor %}
{% 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 %}

View File

@ -37,37 +37,8 @@
{{ _("Download") }}
</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>
{% 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>
<p></p>
{% else %}
<i>
{{ _("No downloads available") }}
@ -76,9 +47,9 @@
{% endblock %}
{% 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") %}
{% elif not package.license.is_foss and package.type != package.type.TXP %}
{% elif not package.license.is_foss %}
{% set package_warning=_("Non-free code") %}
{% elif not package.media_license.is_foss %}
{% set package_warning=_("Non-free media") %}
@ -170,7 +141,7 @@
<span class="count">{{ package.downloads }}</span>
</a>
{% 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>
<span class="count">{{ threads | length }}</span>
</a>
@ -194,12 +165,6 @@
<span class="count">{{ _("Source") }}</span>
</a>
{% 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 %}
<a class="btn" href="{{ package.issueTracker }}">
<i class="fas fa-bug"></i>
@ -265,13 +230,13 @@
{% for ss in screenshots %}
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
<li>
<a href="{{ ss.url }}" class="gallery-image">
<a href="{{ss.url}}" class="gallery-image" data-toggle="modal" data-target="#screenshot_{{ss.id}}">
<img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
{% if not ss.approved %}
<span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span>
{% endif %}
</a>
</li>
</li>
{% endif %}
{% else %}
<li>
@ -290,6 +255,25 @@
</article>
{% 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>
{% from "macros/reviews.html" import render_reviews, render_review_form, render_review_preview with context %}
@ -323,12 +307,10 @@
{{ render_pkggrid(packages_uses) }}
{% endif %}
{% if package.type == package.type.GAME %}
<h2>{{ _("Content") }}</h2>
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary">
{{ _("View content for game") }}
</a>
{% endif %}
<h2>{{ _("Content") }}</h2>
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary">
{{ _("View content for game") }}
</a>
</div>
<aside class="col-md-3 info-sidebar">
@ -378,92 +360,81 @@
</div>
{% endif %}
{% if package.type == package.type.GAME %}
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary">
{{ _("View content for game") }}
</a>
{% endif %}
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary">
{{ _("View content for game") }}
</a>
<h3>{{ _("Dependencies") }}</h3>
<dl>
<dt>{{ _("Required") }}</dt>
<dd>
{% for dep in package.getSortedHardDependencies() %}
{%- if dep.package %}
<a class="badge badge-primary"
href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }}
</a>
{% elif dep.meta_package %}
<a class="badge badge-primary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
</a>
{% else %}
{{ "Expected package or meta_package in dep!" | throw }}
{% endif %}
{% else %}
{{ _("No required dependencies") }}
{% endfor %}
</dd>
{% if package.type != package.type.TXP %}
<h3>{{ _("Dependencies") }}</h3>
<dl>
<dt>{{ _("Required") }}</dt>
{% set optional_deps=package.getSortedOptionalDependencies() %}
{% if optional_deps %}
<dt>{{ _("Optional") }}</dt>
<dd>
{% for dep in package.getSortedHardDependencies() %}
{% for dep in optional_deps %}
{%- if dep.package %}
<a class="badge badge-primary"
<a class="badge badge-secondary"
href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }}
</a>
{% elif dep.meta_package %}
<a class="badge badge-primary"
<a class="badge badge-secondary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
</a>
{% else %}
{{ "Expected package or meta_package in dep!" | throw }}
{% endif %}
{% else %}
{{ _("No required dependencies") }}
{% endif %}</a>
{% endfor %}
</dd>
{% endif %}
</dl>
{% set optional_deps=package.getSortedOptionalDependencies() %}
{% if optional_deps %}
<dt>{{ _("Optional") }}</dt>
<dd>
{% for dep in optional_deps %}
{%- if dep.package %}
<a class="badge badge-secondary"
href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }}
{% elif dep.meta_package %}
<a class="badge badge-secondary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
{% else %}
{{ "Expected package or meta_package in dep!" | throw }}
{% endif %}</a>
{% endfor %}
</dd>
{% endif %}
</dl>
{% endif %}
{% if package.type == package.type.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>{{ _("Compatible Games") }}</h3>
{% for support in package.getSortedSupportedGames() %}
<a class="badge badge-secondary"
href="{{ support.game.getURL('packages.view') }}">
{{ _("%(title)s by %(display_name)s",
title=support.game.title, display_name=support.game.author.display_name) }}
</a>
{% else %}
{{ _("No specific game is required") }}
{% endfor %}
<p class="text-muted small mt-2 mb-0">
{{ _("This is an experimental feature.") }}
{{ _("Supported games are determined by an algorithm, and may not be correct.") }}
</p>
<h3>
{{ _("Information") }}
</h3>
<dl>
<dt>{{ _("Type") }}</dt>
<dd>{{ package.type.text }}</dd>
<dt>{{ _("Technical Name") }}</dt>
<dd>{{ package.name }}</dd>
<dt>{{ _("License") }}</dt>
<dd>
{% if package.license == package.media_license %}
{{ render_license(package.license) }}
{% elif package.type == package.type.TXP %}
{{ render_license(package.media_license) }}
{% else %}
{{ _("%(code_license)s for code,<br>%(media_license)s for media.",
code_license=render_license(package.license), media_license=render_license(package.media_license)) }}
@ -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>
</div>
{% endif %}
{{ _("Threads") }}
{{ _("Discussion") }}
</h3>
<div class="list-group">
{% from "macros/threads.html" import render_compact_threadlist %}
@ -552,5 +523,25 @@
</aside>
</div>
</section>
{% for ss in screenshots %}
<div class="modal fade" id="screenshot_{{ss.id}}" tabindex="-1" aria-labelledby="screenshot" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="screenshot title">"{{ ss.title }}"</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<img src="{{ ss.url }}" alt="{{ ss.title }}" style="width: 100%"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endfor %}
</main>
{% endblock %}

View File

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

View File

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

View File

@ -29,12 +29,6 @@
{{ _("Package Tags") }}
</a>
</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>
</div>
</nav>

View File

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

View File

@ -161,13 +161,6 @@
<p>
{{ medal.description }}
</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>
{% endfor %}

View File

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

View File

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

View File

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

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

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

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