Compare commits
11 Commits
master
...
policy-upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0f8709894 | ||
|
|
a8d71cea12 | ||
|
|
159558a156 | ||
|
|
424a70793a | ||
|
|
439a10526d | ||
|
|
62962fba18 | ||
|
|
95d45802f7 | ||
|
|
3229e5420a | ||
|
|
4e5ec0a486 | ||
|
|
66dcf0082a | ||
|
|
155a2af8bd |
@@ -1,7 +1,7 @@
|
|||||||
# ContentDB
|
# ContentDB
|
||||||

|

|
||||||
|
|
||||||
A content database for Luanti mods, games, and more.\
|
A content database for Minetest mods, games, and more.\
|
||||||
Developed by rubenwardy, license AGPLv3.0+.
|
Developed by rubenwardy, license AGPLv3.0+.
|
||||||
|
|
||||||
See [Getting Started](docs/getting_started.md) for setting up a development/prodiction environment.
|
See [Getting Started](docs/getting_started.md) for setting up a development/prodiction environment.
|
||||||
@@ -82,7 +82,7 @@ Package "1" --> "*" Release
|
|||||||
Package "1" --> "*" Dependency
|
Package "1" --> "*" Dependency
|
||||||
Package "1" --> "*" Tag
|
Package "1" --> "*" Tag
|
||||||
Package "1" --> "*" MetaPackage : provides
|
Package "1" --> "*" MetaPackage : provides
|
||||||
Release --> LuantiVersion
|
Release --> MinetestVersion
|
||||||
Package --> License
|
Package --> License
|
||||||
Dependency --> Package
|
Dependency --> Package
|
||||||
Dependency --> MetaPackage
|
Dependency --> MetaPackage
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ pgettext("tags", "For mods created for the Discord \"Weekly Challenges\" modding
|
|||||||
# NOTE: tags: title for less_than_px
|
# NOTE: tags: title for less_than_px
|
||||||
pgettext("tags", "<16px")
|
pgettext("tags", "<16px")
|
||||||
# NOTE: tags: description for less_than_px
|
# NOTE: tags: description for less_than_px
|
||||||
pgettext("tags", "For less than 16px texture packs ")
|
pgettext("tags", "Less than 16px")
|
||||||
# NOTE: tags: title for library
|
# NOTE: tags: title for library
|
||||||
pgettext("tags", "API / Library")
|
pgettext("tags", "API / Library")
|
||||||
# NOTE: tags: description for library
|
# NOTE: tags: description for library
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ from typing import List
|
|||||||
import requests
|
import requests
|
||||||
from celery import group, uuid
|
from celery import group, uuid
|
||||||
from flask import redirect, url_for, flash, current_app
|
from flask import redirect, url_for, flash, current_app
|
||||||
from sqlalchemy import or_, and_, not_, func
|
from sqlalchemy import or_, and_
|
||||||
|
|
||||||
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
|
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
|
||||||
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, Thread, AuditLogEntry, ReportAttachment
|
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, Thread, AuditLogEntry
|
||||||
from app.tasks.emails import send_pending_digests
|
from app.tasks.emails import send_pending_digests
|
||||||
from app.tasks.forumtasks import import_topic_list, check_all_forum_accounts
|
from app.tasks.forumtasks import import_topic_list, check_all_forum_accounts
|
||||||
from app.tasks.importtasks import import_repo_screenshot, check_zip_release, check_for_updates, update_all_game_support, \
|
from app.tasks.importtasks import import_repo_screenshot, check_zip_release, check_for_updates, update_all_game_support, \
|
||||||
import_languages, check_all_zip_files
|
import_languages, check_all_zip_files
|
||||||
from app.tasks.usertasks import import_github_user_ids, do_delete_likely_spammers
|
from app.tasks.usertasks import import_github_user_ids
|
||||||
from app.tasks.pkgtasks import notify_about_git_forum_links, clear_removed_packages, check_package_for_broken_links, update_file_size_bytes
|
from app.tasks.pkgtasks import notify_about_git_forum_links, clear_removed_packages, check_package_for_broken_links
|
||||||
from app.utils import add_notification, get_system_user
|
from app.utils import add_notification, get_system_user
|
||||||
|
|
||||||
actions = {}
|
actions = {}
|
||||||
@@ -68,10 +68,9 @@ def clean_uploads():
|
|||||||
|
|
||||||
release_urls = get_filenames_from_column(PackageRelease.url)
|
release_urls = get_filenames_from_column(PackageRelease.url)
|
||||||
screenshot_urls = get_filenames_from_column(PackageScreenshot.url)
|
screenshot_urls = get_filenames_from_column(PackageScreenshot.url)
|
||||||
attachment_urls = get_filenames_from_column(ReportAttachment.url)
|
|
||||||
pp_urls = get_filenames_from_column(User.profile_pic)
|
pp_urls = get_filenames_from_column(User.profile_pic)
|
||||||
|
|
||||||
db_urls = release_urls.union(screenshot_urls).union(pp_urls).union(attachment_urls)
|
db_urls = release_urls.union(screenshot_urls).union(pp_urls)
|
||||||
unreachable = existing_uploads.difference(db_urls)
|
unreachable = existing_uploads.difference(db_urls)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -323,13 +322,6 @@ def do_check_all_zip_files():
|
|||||||
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
|
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
|
||||||
|
|
||||||
|
|
||||||
@action("Update file_size_bytes")
|
|
||||||
def do_update_file_size_bytes():
|
|
||||||
task_id = uuid()
|
|
||||||
update_file_size_bytes.apply_async((), task_id=task_id)
|
|
||||||
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
|
|
||||||
|
|
||||||
|
|
||||||
@action("DANGER: Delete less popular removed packages")
|
@action("DANGER: Delete less popular removed packages")
|
||||||
def del_less_popular_removed_packages():
|
def del_less_popular_removed_packages():
|
||||||
task_id = uuid()
|
task_id = uuid()
|
||||||
@@ -425,10 +417,3 @@ def delete_empty_threads():
|
|||||||
def check_for_broken_links():
|
def check_for_broken_links():
|
||||||
for package in Package.query.filter_by(state=PackageState.APPROVED).all():
|
for package in Package.query.filter_by(state=PackageState.APPROVED).all():
|
||||||
check_package_for_broken_links.delay(package.id)
|
check_package_for_broken_links.delay(package.id)
|
||||||
|
|
||||||
|
|
||||||
@action("DANGER: Delete likely spammers")
|
|
||||||
def delete_likely_spammers():
|
|
||||||
task_id = uuid()
|
|
||||||
do_delete_likely_spammers.apply_async((), task_id=task_id)
|
|
||||||
return redirect(url_for("tasks.check", id=task_id, r=url_for("admin.admin_page")))
|
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ from wtforms import StringField, SubmitField, BooleanField
|
|||||||
from wtforms.validators import InputRequired, Length, Optional
|
from wtforms.validators import InputRequired, Length, Optional
|
||||||
from app.utils import rank_required, add_audit_log, add_notification, get_system_user, nonempty_or_none, \
|
from app.utils import rank_required, add_audit_log, add_notification, get_system_user, nonempty_or_none, \
|
||||||
get_int_or_abort
|
get_int_or_abort
|
||||||
from sqlalchemy import func
|
|
||||||
from . import bp
|
from . import bp
|
||||||
from .actions import actions
|
from .actions import actions
|
||||||
from app.models import UserRank, Package, db, PackageState, PackageRelease, PackageScreenshot, User, AuditSeverity, NotificationType, PackageAlias
|
from app.models import UserRank, Package, db, PackageState, User, AuditSeverity, NotificationType, PackageAlias
|
||||||
from ...querybuilder import QueryBuilder
|
from ...querybuilder import QueryBuilder
|
||||||
|
|
||||||
|
|
||||||
@@ -183,17 +182,6 @@ def transfer():
|
|||||||
return render_template("admin/transfer.html", form=form)
|
return render_template("admin/transfer.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
def sum_file_sizes(clazz):
|
|
||||||
ret = {}
|
|
||||||
for entry in (db.session
|
|
||||||
.query(clazz.package_id, func.sum(clazz.file_size_bytes))
|
|
||||||
.select_from(clazz)
|
|
||||||
.group_by(clazz.package_id)
|
|
||||||
.all()):
|
|
||||||
ret[entry[0]] = entry[1]
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/admin/storage/")
|
@bp.route("/admin/storage/")
|
||||||
@rank_required(UserRank.EDITOR)
|
@rank_required(UserRank.EDITOR)
|
||||||
def storage():
|
def storage():
|
||||||
@@ -204,20 +192,15 @@ def storage():
|
|||||||
show_all = len(packages) < 100
|
show_all = len(packages) < 100
|
||||||
min_size = get_int_or_abort(request.args.get("min_size"), 0 if show_all else 50)
|
min_size = get_int_or_abort(request.args.get("min_size"), 0 if show_all else 50)
|
||||||
|
|
||||||
package_size_releases = sum_file_sizes(PackageRelease)
|
|
||||||
package_size_screenshots = sum_file_sizes(PackageScreenshot)
|
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for package in packages:
|
for package in packages:
|
||||||
size_releases = package_size_releases.get(package.id, 0)
|
size_releases = sum([x.file_size_bytes for x in package.releases])
|
||||||
size_screenshots = package_size_screenshots.get(package.id, 0)
|
size_screenshots = sum([x.file_size_bytes for x in package.screenshots])
|
||||||
size_total = size_releases + size_screenshots
|
|
||||||
if size_total < min_size * 1024 * 1024:
|
|
||||||
continue
|
|
||||||
|
|
||||||
latest_release = package.releases.first()
|
latest_release = package.releases.first()
|
||||||
size_latest = latest_release.file_size_bytes if latest_release else 0
|
size_latest = latest_release.file_size_bytes if latest_release else 0
|
||||||
data.append([package, size_total, size_releases, size_screenshots, size_latest])
|
size_total = size_releases + size_screenshots
|
||||||
|
if size_total > min_size*1024*1024:
|
||||||
|
data.append([package, size_total, size_releases, size_screenshots, size_latest])
|
||||||
|
|
||||||
data.sort(key=lambda x: x[1], reverse=True)
|
data.sort(key=lambda x: x[1], reverse=True)
|
||||||
return render_template("admin/storage.html", data=data)
|
return render_template("admin/storage.html", data=data)
|
||||||
|
|||||||
@@ -15,11 +15,7 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from flask import render_template, request, abort
|
from flask import render_template, request, abort
|
||||||
from flask_babel import lazy_gettext
|
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import StringField, SubmitField
|
|
||||||
from wtforms.validators import Optional, Length
|
|
||||||
|
|
||||||
from app.models import db, AuditLogEntry, UserRank, User, Permission
|
from app.models import db, AuditLogEntry, UserRank, User, Permission
|
||||||
from app.utils import rank_required, get_int_or_abort
|
from app.utils import rank_required, get_int_or_abort
|
||||||
@@ -27,40 +23,26 @@ from app.utils import rank_required, get_int_or_abort
|
|||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
class AuditForm(FlaskForm):
|
|
||||||
username = StringField(lazy_gettext("Username"), [Optional(), Length(0, 25)])
|
|
||||||
q = StringField(lazy_gettext("Query"), [Optional(), Length(0, 300)])
|
|
||||||
url = StringField(lazy_gettext("URL"), [Optional(), Length(0, 300)])
|
|
||||||
submit = SubmitField(lazy_gettext("Search"), name=None)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/admin/audit/")
|
@bp.route("/admin/audit/")
|
||||||
@rank_required(UserRank.APPROVER)
|
@rank_required(UserRank.MODERATOR)
|
||||||
def audit():
|
def audit():
|
||||||
page = get_int_or_abort(request.args.get("page"), 1)
|
page = get_int_or_abort(request.args.get("page"), 1)
|
||||||
num = min(40, get_int_or_abort(request.args.get("n"), 100))
|
num = min(40, get_int_or_abort(request.args.get("n"), 100))
|
||||||
|
|
||||||
query = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at))
|
query = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at))
|
||||||
|
|
||||||
form = AuditForm(request.args)
|
if "username" in request.args:
|
||||||
username = form.username.data
|
user = User.query.filter_by(username=request.args.get("username")).first()
|
||||||
q = form.q.data
|
if not user:
|
||||||
url = form.url.data
|
abort(404)
|
||||||
if username:
|
|
||||||
user = User.query.filter_by(username=username).first_or_404()
|
|
||||||
query = query.filter_by(causer=user)
|
query = query.filter_by(causer=user)
|
||||||
|
|
||||||
if q:
|
if "q" in request.args:
|
||||||
|
q = request.args["q"]
|
||||||
query = query.filter(AuditLogEntry.title.ilike(f"%{q}%"))
|
query = query.filter(AuditLogEntry.title.ilike(f"%{q}%"))
|
||||||
|
|
||||||
if url:
|
|
||||||
query = query.filter(AuditLogEntry.url.ilike(f"%{url}%"))
|
|
||||||
|
|
||||||
if not current_user.rank.at_least(UserRank.MODERATOR):
|
|
||||||
query = query.filter(AuditLogEntry.package)
|
|
||||||
|
|
||||||
pagination = query.paginate(page=page, per_page=num)
|
pagination = query.paginate(page=page, per_page=num)
|
||||||
return render_template("admin/audit.html", log=pagination.items, pagination=pagination, form=form)
|
return render_template("admin/audit.html", log=pagination.items, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/admin/audit/<int:id_>/")
|
@bp.route("/admin/audit/<int:id_>/")
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ from wtforms.validators import InputRequired, Length
|
|||||||
|
|
||||||
from app.utils import rank_required, add_audit_log
|
from app.utils import rank_required, add_audit_log
|
||||||
from . import bp
|
from . import bp
|
||||||
from app.models import UserRank, LuantiRelease, db, AuditSeverity
|
from app.models import UserRank, MinetestRelease, db, AuditSeverity
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/versions/")
|
@bp.route("/versions/")
|
||||||
@rank_required(UserRank.MODERATOR)
|
@rank_required(UserRank.MODERATOR)
|
||||||
def version_list():
|
def version_list():
|
||||||
return render_template("admin/versions/list.html",
|
return render_template("admin/versions/list.html",
|
||||||
versions=LuantiRelease.query.order_by(db.asc(LuantiRelease.id)).all())
|
versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
|
||||||
|
|
||||||
|
|
||||||
class VersionForm(FlaskForm):
|
class VersionForm(FlaskForm):
|
||||||
@@ -45,14 +45,14 @@ class VersionForm(FlaskForm):
|
|||||||
def create_edit_version(name=None):
|
def create_edit_version(name=None):
|
||||||
version = None
|
version = None
|
||||||
if name is not None:
|
if name is not None:
|
||||||
version = LuantiRelease.query.filter_by(name=name).first()
|
version = MinetestRelease.query.filter_by(name=name).first()
|
||||||
if version is None:
|
if version is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
form = VersionForm(formdata=request.form, obj=version)
|
form = VersionForm(formdata=request.form, obj=version)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
if version is None:
|
if version is None:
|
||||||
version = LuantiRelease(form.name.data)
|
version = MinetestRelease(form.name.data)
|
||||||
db.session.add(version)
|
db.session.add(version)
|
||||||
flash("Created version " + form.name.data, "success")
|
flash("Created version " + form.name.data, "success")
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ from app import csrf
|
|||||||
from app.logic.graphs import get_package_stats, get_package_stats_for_user, get_all_package_stats
|
from app.logic.graphs import get_package_stats, get_package_stats_for_user, get_all_package_stats
|
||||||
from app.markdown import render_markdown
|
from app.markdown import render_markdown
|
||||||
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, \
|
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, \
|
||||||
LuantiRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread, Collection, \
|
MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread, Collection, \
|
||||||
PackageAlias, Language
|
PackageAlias, Language
|
||||||
from app.querybuilder import QueryBuilder
|
from app.querybuilder import QueryBuilder
|
||||||
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, is_yes, get_request_date, cached, \
|
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, is_yes, get_request_date, cached, \
|
||||||
cors_allowed
|
cors_allowed
|
||||||
from app.utils.luanti_hypertext import html_to_luanti, package_info_as_hypertext, package_reviews_as_hypertext
|
from app.utils.minetest_hypertext import html_to_minetest, package_info_as_hypertext, package_reviews_as_hypertext
|
||||||
from . import bp
|
from . import bp
|
||||||
from .auth import is_api_authd
|
from .auth import is_api_authd
|
||||||
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
|
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
|
||||||
@@ -102,7 +102,7 @@ def package_view_client(package: Package):
|
|||||||
protocol_version = request.args.get("protocol_version")
|
protocol_version = request.args.get("protocol_version")
|
||||||
engine_version = request.args.get("engine_version")
|
engine_version = request.args.get("engine_version")
|
||||||
if protocol_version or engine_version:
|
if protocol_version or engine_version:
|
||||||
version = LuantiRelease.get(engine_version, get_int_or_abort(protocol_version))
|
version = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version))
|
||||||
else:
|
else:
|
||||||
version = None
|
version = None
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ def package_view_client(package: Package):
|
|||||||
page_url = package.get_url("packages.view", absolute=True)
|
page_url = package.get_url("packages.view", absolute=True)
|
||||||
if data["long_description"] is not None:
|
if data["long_description"] is not None:
|
||||||
html = render_markdown(data["long_description"])
|
html = render_markdown(data["long_description"])
|
||||||
data["long_description"] = html_to_luanti(html, page_url, formspec_version, include_images)
|
data["long_description"] = html_to_minetest(html, page_url, formspec_version, include_images)
|
||||||
|
|
||||||
data["info_hypertext"] = package_info_as_hypertext(package, formspec_version)
|
data["info_hypertext"] = package_info_as_hypertext(package, formspec_version)
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ def package_hypertext(package):
|
|||||||
include_images = is_yes(request.args.get("include_images", "true"))
|
include_images = is_yes(request.args.get("include_images", "true"))
|
||||||
html = render_markdown(package.desc if package.desc else "")
|
html = render_markdown(package.desc if package.desc else "")
|
||||||
page_url = package.get_url("packages.view", absolute=True)
|
page_url = package.get_url("packages.view", absolute=True)
|
||||||
return jsonify(html_to_luanti(html, page_url, formspec_version, include_images))
|
return jsonify(html_to_minetest(html, page_url, formspec_version, include_images))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/packages/<author>/<name>/", methods=["PUT"])
|
@bp.route("/api/packages/<author>/<name>/", methods=["PUT"])
|
||||||
@@ -636,14 +636,14 @@ def versions():
|
|||||||
protocol_version = request.args.get("protocol_version")
|
protocol_version = request.args.get("protocol_version")
|
||||||
engine_version = request.args.get("engine_version")
|
engine_version = request.args.get("engine_version")
|
||||||
if protocol_version or engine_version:
|
if protocol_version or engine_version:
|
||||||
rel = LuantiRelease.get(engine_version, get_int_or_abort(protocol_version))
|
rel = MinetestRelease.get(engine_version, get_int_or_abort(protocol_version))
|
||||||
if rel is None:
|
if rel is None:
|
||||||
error(404, "No releases found")
|
error(404, "No releases found")
|
||||||
|
|
||||||
return jsonify(rel.as_dict())
|
return jsonify(rel.as_dict())
|
||||||
|
|
||||||
return jsonify([rel.as_dict() \
|
return jsonify([rel.as_dict() \
|
||||||
for rel in LuantiRelease.query.all() if rel.get_actual() is not None])
|
for rel in MinetestRelease.query.all() if rel.get_actual() is not None])
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/languages/")
|
@bp.route("/api/languages/")
|
||||||
@@ -835,7 +835,7 @@ def hypertext():
|
|||||||
if request.content_type == "text/markdown":
|
if request.content_type == "text/markdown":
|
||||||
html = render_markdown(html)
|
html = render_markdown(html)
|
||||||
|
|
||||||
return jsonify(html_to_luanti(html, "", formspec_version, include_images))
|
return jsonify(html_to_minetest(html, "", formspec_version, include_images))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/collections/")
|
@bp.route("/api/collections/")
|
||||||
@@ -886,9 +886,9 @@ def collection_view(token, author, name):
|
|||||||
@cached(300)
|
@cached(300)
|
||||||
def updates():
|
def updates():
|
||||||
protocol_version = get_int_or_abort(request.args.get("protocol_version"))
|
protocol_version = get_int_or_abort(request.args.get("protocol_version"))
|
||||||
engine_version = request.args.get("engine_version")
|
minetest_version = request.args.get("engine_version")
|
||||||
if protocol_version or engine_version:
|
if protocol_version or minetest_version:
|
||||||
version = LuantiRelease.get(engine_version, protocol_version)
|
version = MinetestRelease.get(minetest_version, protocol_version)
|
||||||
else:
|
else:
|
||||||
version = None
|
version = None
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from flask import jsonify, abort, make_response, url_for, current_app
|
|||||||
from app.logic.packages import do_edit_package
|
from app.logic.packages import do_edit_package
|
||||||
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
|
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
|
||||||
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image
|
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image
|
||||||
from app.models import APIToken, Package, LuantiRelease, PackageScreenshot
|
from app.models import APIToken, Package, MinetestRelease, PackageScreenshot
|
||||||
|
|
||||||
|
|
||||||
def error(code: int, msg: str):
|
def error(code: int, msg: str):
|
||||||
@@ -39,7 +39,7 @@ def guard(f):
|
|||||||
|
|
||||||
|
|
||||||
def api_create_vcs_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
|
def api_create_vcs_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
|
||||||
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason="API"):
|
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API"):
|
||||||
if not token.can_operate_on_package(package):
|
if not token.can_operate_on_package(package):
|
||||||
error(403, "API token does not have access to the package")
|
error(403, "API token does not have access to the package")
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ def api_create_vcs_release(token: APIToken, package: Package, name: str, title:
|
|||||||
|
|
||||||
|
|
||||||
def api_create_zip_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
|
def api_create_zip_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
|
||||||
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason="API", commit_hash: str = None):
|
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API", commit_hash: str = None):
|
||||||
if not token.can_operate_on_package(package):
|
if not token.can_operate_on_package(package):
|
||||||
error(403, "API token does not have access to the package")
|
error(403, "API token does not have access to the package")
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from sqlalchemy.sql.expression import func
|
|||||||
|
|
||||||
PKGS_PER_ROW = 4
|
PKGS_PER_ROW = 4
|
||||||
|
|
||||||
# GAMEJAM_BANNER = "https://jam.luanti.org/img/banner.png"
|
# GAMEJAM_BANNER = "https://jam.minetest.net/img/banner.png"
|
||||||
#
|
#
|
||||||
# class GameJam:
|
# class GameJam:
|
||||||
# cover_image = type("", (), dict(url=GAMEJAM_BANNER))()
|
# cover_image = type("", (), dict(url=GAMEJAM_BANNER))()
|
||||||
@@ -40,7 +40,7 @@ PKGS_PER_ROW = 4
|
|||||||
# def get_url(self, _name):
|
# def get_url(self, _name):
|
||||||
# return "/gamejam/"
|
# return "/gamejam/"
|
||||||
#
|
#
|
||||||
# title = "Luanti Game Jam 2023: \"Unexpected\""
|
# title = "Minetest Game Jam 2023: \"Unexpected\""
|
||||||
# author = None
|
# author = None
|
||||||
#
|
#
|
||||||
# short_desc = "The game jam has finished! It's now up to the community to play and rate the games."
|
# short_desc = "The game jam has finished! It's now up to the community to play and rate the games."
|
||||||
@@ -51,7 +51,7 @@ PKGS_PER_ROW = 4
|
|||||||
|
|
||||||
@bp.route("/gamejam/")
|
@bp.route("/gamejam/")
|
||||||
def gamejam():
|
def gamejam():
|
||||||
return redirect("https://jam.luanti.org/")
|
return redirect("https://jam.minetest.net/")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from wtforms.validators import Optional
|
|||||||
from wtforms_sqlalchemy.fields import QuerySelectMultipleField, QuerySelectField
|
from wtforms_sqlalchemy.fields import QuerySelectMultipleField, QuerySelectField
|
||||||
|
|
||||||
from . import bp
|
from . import bp
|
||||||
from ...models import PackageType, Tag, db, ContentWarning, License, Language, LuantiRelease, Package, PackageState
|
from ...models import PackageType, Tag, db, ContentWarning, License, Language, MinetestRelease, Package, PackageState
|
||||||
|
|
||||||
|
|
||||||
def make_label(obj: Tag | ContentWarning):
|
def make_label(obj: Tag | ContentWarning):
|
||||||
@@ -75,7 +75,7 @@ class AdvancedSearchForm(FlaskForm):
|
|||||||
get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
||||||
hide = SelectMultipleField(lazy_gettext("Hide Tags and Content Warnings"), [Optional()])
|
hide = SelectMultipleField(lazy_gettext("Hide Tags and Content Warnings"), [Optional()])
|
||||||
engine_version = QuerySelectField(lazy_gettext("Luanti Version"),
|
engine_version = QuerySelectField(lazy_gettext("Luanti Version"),
|
||||||
query_factory=lambda: LuantiRelease.query.order_by(db.asc(LuantiRelease.id)),
|
query_factory=lambda: MinetestRelease.query.order_by(db.asc(MinetestRelease.id)),
|
||||||
allow_blank=True, blank_value="",
|
allow_blank=True, blank_value="",
|
||||||
get_pk=lambda a: a.value, get_label=lambda a: a.name)
|
get_pk=lambda a: a.value, get_label=lambda a: a.name)
|
||||||
sort = SelectField(lazy_gettext("Sort by"), [Optional()], choices=[
|
sort = SelectField(lazy_gettext("Sort by"), [Optional()], choices=[
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ class PackageForm(FlaskForm):
|
|||||||
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-z0-9_]+$", 0, lazy_gettext("Lower case letters (a-z), digits (0-9), and underscores (_) only"))])
|
||||||
short_desc = StringField(lazy_gettext("Short Description (Plaintext)"), [InputRequired(), Length(1,200)])
|
short_desc = StringField(lazy_gettext("Short Description (Plaintext)"), [InputRequired(), Length(1,200)])
|
||||||
|
|
||||||
dev_state = SelectField(lazy_gettext("Maintenance State"), [DataRequired()], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce)
|
dev_state = SelectField(lazy_gettext("Maintenance State"), [InputRequired()], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce)
|
||||||
|
|
||||||
tags = QuerySelectMultipleField(lazy_gettext('Tags'), query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=make_label)
|
tags = QuerySelectMultipleField(lazy_gettext('Tags'), query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=make_label)
|
||||||
content_warnings = QuerySelectMultipleField(lazy_gettext('Content Warnings'), query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=make_label)
|
content_warnings = QuerySelectMultipleField(lazy_gettext('Content Warnings'), query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=make_label)
|
||||||
@@ -306,6 +306,10 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
|
|||||||
"translation_url": form.translation_url.data,
|
"translation_url": form.translation_url.data,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if wasNew:
|
||||||
|
msg = f"Created package {author.username}/{form.name.data}"
|
||||||
|
add_audit_log(AuditSeverity.NORMAL, current_user, msg, package.get_url("packages.view"), package)
|
||||||
|
|
||||||
if wasNew and package.repo is not None:
|
if wasNew and package.repo is not None:
|
||||||
import_repo_screenshot.delay(package.id)
|
import_repo_screenshot.delay(package.id)
|
||||||
|
|
||||||
@@ -318,14 +322,13 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
|
|||||||
return redirect(next_url)
|
return redirect(next_url)
|
||||||
except LogicError as e:
|
except LogicError as e:
|
||||||
flash(e.message, "danger")
|
flash(e.message, "danger")
|
||||||
db.session.rollback()
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/packages/new/", methods=["GET", "POST"])
|
@bp.route("/packages/new/", methods=["GET", "POST"])
|
||||||
@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
|
@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def create_edit(author=None, name=None):
|
def create_edit(author=None, name=None):
|
||||||
if current_user.email is None and not current_user.rank.at_least(UserRank.ADMIN):
|
if current_user.email is None:
|
||||||
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
|
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
|
||||||
return redirect(url_for("users.email_notifications"))
|
return redirect(url_for("users.email_notifications"))
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from wtforms.validators import InputRequired, Length, Optional
|
|||||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||||
|
|
||||||
from app.logic.releases import do_create_vcs_release, LogicError, do_create_zip_release
|
from app.logic.releases import do_create_vcs_release, LogicError, do_create_zip_release
|
||||||
from app.models import Package, db, User, PackageState, Permission, UserRank, PackageDailyStats, LuantiRelease, \
|
from app.models import Package, db, User, PackageState, Permission, UserRank, PackageDailyStats, MinetestRelease, \
|
||||||
PackageRelease, PackageUpdateTrigger, PackageUpdateConfig
|
PackageRelease, PackageUpdateTrigger, PackageUpdateConfig
|
||||||
from app.rediscache import has_key, set_temp_key, make_download_key
|
from app.rediscache import has_key, set_temp_key, make_download_key
|
||||||
from app.tasks.importtasks import check_update_config
|
from app.tasks.importtasks import check_update_config
|
||||||
@@ -42,11 +42,11 @@ def list_releases(package):
|
|||||||
|
|
||||||
|
|
||||||
def get_mt_releases(is_max):
|
def get_mt_releases(is_max):
|
||||||
query = LuantiRelease.query.order_by(db.asc(LuantiRelease.id))
|
query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
|
||||||
if is_max:
|
if is_max:
|
||||||
query = query.limit(query.count() - 1)
|
query = query.limit(query.count() - 1)
|
||||||
else:
|
else:
|
||||||
query = query.filter(LuantiRelease.name != "0.4.17")
|
query = query.filter(MinetestRelease.name != "0.4.17")
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class EditPackageReleaseForm(FlaskForm):
|
|||||||
@login_required
|
@login_required
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def create_release(package):
|
def create_release(package):
|
||||||
if current_user.email is None and not current_user.rank.at_least(UserRank.ADMIN):
|
if current_user.email is None:
|
||||||
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
|
flash(gettext("You must add an email address to your account and confirm it before you can manage packages"), "danger")
|
||||||
return redirect(url_for("users.email_notifications"))
|
return redirect(url_for("users.email_notifications"))
|
||||||
|
|
||||||
@@ -128,9 +128,9 @@ def download_release(package, id):
|
|||||||
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
|
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||||
if ip is not None and not is_user_bot():
|
if ip is not None and not is_user_bot():
|
||||||
user_agent = request.headers.get("User-Agent") or ""
|
user_agent = request.headers.get("User-Agent") or ""
|
||||||
is_luanti = user_agent.startswith("Luanti") or user_agent.startswith("Minetest")
|
is_minetest = user_agent.startswith("Luanti") or user_agent.startswith("Minetest")
|
||||||
reason = request.args.get("reason")
|
reason = request.args.get("reason")
|
||||||
PackageDailyStats.update(package, is_luanti, reason)
|
PackageDailyStats.update(package, is_minetest, reason)
|
||||||
|
|
||||||
key = make_download_key(ip, release.package)
|
key = make_download_key(ip, release.package)
|
||||||
if not has_key(key):
|
if not has_key(key):
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ from flask import render_template, request, redirect, flash, url_for, abort
|
|||||||
from flask_babel import lazy_gettext, gettext
|
from flask_babel import lazy_gettext, gettext
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileRequired
|
|
||||||
from wtforms import StringField, SubmitField, BooleanField, FileField
|
from wtforms import StringField, SubmitField, BooleanField, FileField
|
||||||
from wtforms.validators import Length, DataRequired, Optional
|
from wtforms.validators import InputRequired, Length, DataRequired, Optional
|
||||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||||
|
|
||||||
from app.logic.LogicError import LogicError
|
from app.logic.LogicError import LogicError
|
||||||
@@ -33,7 +32,7 @@ from app.utils import is_package_page
|
|||||||
|
|
||||||
class CreateScreenshotForm(FlaskForm):
|
class CreateScreenshotForm(FlaskForm):
|
||||||
title = StringField(lazy_gettext("Title/Caption"), [Optional(), Length(-1, 100)])
|
title = StringField(lazy_gettext("Title/Caption"), [Optional(), Length(-1, 100)])
|
||||||
file_upload = FileField(lazy_gettext("File Upload"), [FileRequired()])
|
file_upload = FileField(lazy_gettext("File Upload"), [InputRequired()])
|
||||||
submit = SubmitField(lazy_gettext("Save"))
|
submit = SubmitField(lazy_gettext("Save"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,30 +14,24 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from flask import Blueprint, request, render_template, url_for, abort, flash
|
from flask import Blueprint, request, render_template, url_for, abort
|
||||||
from flask_babel import lazy_gettext
|
from flask_babel import lazy_gettext
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
from wtforms import TextAreaField, SubmitField, URLField, StringField, SelectField, FileField
|
from wtforms import TextAreaField, SubmitField
|
||||||
from wtforms.validators import InputRequired, Length, Optional, DataRequired
|
from wtforms.validators import InputRequired, Length
|
||||||
|
|
||||||
from app.logic.uploads import upload_file
|
from app.models import User, UserRank
|
||||||
from app.models import User, UserRank, Report, db, AuditSeverity, ReportCategory, Thread, Permission, ReportAttachment
|
from app.tasks.emails import send_user_email
|
||||||
from app.tasks.webhooktasks import post_discord_webhook
|
from app.tasks.webhooktasks import post_discord_webhook
|
||||||
from app.utils import (is_no, abs_url_samesite, normalize_line_endings, rank_required, add_audit_log, abs_url_for,
|
from app.utils import is_no, abs_url_samesite, normalize_line_endings
|
||||||
random_string, add_replies)
|
|
||||||
|
|
||||||
bp = Blueprint("report", __name__)
|
bp = Blueprint("report", __name__)
|
||||||
|
|
||||||
|
|
||||||
class ReportForm(FlaskForm):
|
class ReportForm(FlaskForm):
|
||||||
category = SelectField(lazy_gettext("Category"), [DataRequired()], choices=ReportCategory.choices(with_none=True), coerce=ReportCategory.coerce)
|
message = TextAreaField(lazy_gettext("Message"), [InputRequired(), Length(10, 10000)], filters=[normalize_line_endings])
|
||||||
|
|
||||||
url = URLField(lazy_gettext("URL"), [Optional()])
|
|
||||||
title = StringField(lazy_gettext("Subject / Title"), [InputRequired(), Length(10, 300)])
|
|
||||||
message = TextAreaField(lazy_gettext("Message"), [Optional(), Length(0, 10000)], filters=[normalize_line_endings])
|
|
||||||
file_upload = FileField(lazy_gettext("Image Upload"), [Optional()])
|
|
||||||
submit = SubmitField(lazy_gettext("Report"))
|
submit = SubmitField(lazy_gettext("Report"))
|
||||||
|
|
||||||
|
|
||||||
@@ -52,131 +46,25 @@ def report():
|
|||||||
|
|
||||||
url = abs_url_samesite(url)
|
url = abs_url_samesite(url)
|
||||||
|
|
||||||
form = ReportForm() if current_user.is_authenticated else None
|
form = ReportForm(formdata=request.form) if current_user.is_authenticated else None
|
||||||
if form and request.method == "GET":
|
if form and request.method == "GET":
|
||||||
try:
|
form.message.data = request.args.get("message", "")
|
||||||
form.category.data = ReportCategory.coerce(request.args.get("category"))
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
form.url.data = url
|
|
||||||
form.title.data = request.args.get("title", "")
|
|
||||||
|
|
||||||
if form and form.validate_on_submit():
|
if form and form.validate_on_submit():
|
||||||
report = Report()
|
|
||||||
report.id = random_string(8)
|
|
||||||
report.user = current_user if current_user.is_authenticated else None
|
|
||||||
form.populate_obj(report)
|
|
||||||
|
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
thread = Thread()
|
user_info = f"{current_user.username}"
|
||||||
thread.title = f"Report: {form.title.data}"
|
|
||||||
thread.author = current_user
|
|
||||||
thread.private = True
|
|
||||||
thread.watchers.extend(User.query.filter(User.rank >= UserRank.MODERATOR).all())
|
|
||||||
db.session.add(thread)
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
report.thread = thread
|
|
||||||
|
|
||||||
add_replies(thread, current_user, f"**{report.category.title} report created**\n\n{form.message.data}")
|
|
||||||
else:
|
else:
|
||||||
ip_addr = request.headers.get("X-Forwarded-For") or request.remote_addr
|
user_info = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||||
report.message = ip_addr + "\n\n" + report.message
|
|
||||||
|
|
||||||
db.session.add(report)
|
text = f"{url}\n\n{form.message.data}"
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
if form.file_upload.data:
|
task = None
|
||||||
atmt = ReportAttachment()
|
for admin in User.query.filter_by(rank=UserRank.ADMIN).all():
|
||||||
report.attachments.add(atmt)
|
task = send_user_email.delay(admin.email, admin.locale or "en",
|
||||||
uploaded_url, _ = upload_file(form.file_upload.data, "image", lazy_gettext("a PNG, JPEG, or WebP image file"))
|
f"User report from {user_info}", text)
|
||||||
atmt.url = uploaded_url
|
|
||||||
db.session.add(atmt)
|
|
||||||
|
|
||||||
if current_user.is_authenticated:
|
post_discord_webhook.delay(None if is_anon else current_user.username, f"**New Report**\n{url}\n\n{form.message.data}", True)
|
||||||
add_audit_log(AuditSeverity.USER, current_user, f"New report: {report.title}",
|
|
||||||
url_for("report.view", rid=report.id))
|
|
||||||
|
|
||||||
db.session.commit()
|
return redirect(url_for("tasks.check", id=task.id, r=url_for("homepage.home")))
|
||||||
|
|
||||||
abs_url = abs_url_for("report.view", rid=report.id)
|
return render_template("report/index.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
|
||||||
msg = f"**New Report**\nReport on `{report.url}`\n\n{report.title}\n\nView: {abs_url}"
|
|
||||||
post_discord_webhook.delay(None if is_anon else current_user.username, msg, True)
|
|
||||||
|
|
||||||
return redirect(url_for("report.report_received", rid=report.id))
|
|
||||||
|
|
||||||
return render_template("report/report.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/report/received/")
|
|
||||||
def report_received():
|
|
||||||
rid = request.args.get("rid")
|
|
||||||
report = Report.query.get_or_404(rid)
|
|
||||||
return render_template("report/report_received.html", report=report)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/admin/reports/")
|
|
||||||
@rank_required(UserRank.EDITOR)
|
|
||||||
def list_all():
|
|
||||||
reports = Report.query.order_by(db.asc(Report.is_resolved), db.asc(Report.created_at)).all()
|
|
||||||
return render_template("report/list.html", reports=reports)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/admin/reports/<rid>/", methods=["GET", "POST"])
|
|
||||||
def view(rid: str):
|
|
||||||
report = Report.query.get_or_404(rid)
|
|
||||||
if not report.check_perm(current_user, Permission.SEE_REPORT):
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
if report.is_resolved:
|
|
||||||
if "reopen" in request.form:
|
|
||||||
report.is_resolved = False
|
|
||||||
url = url_for("report.view", rid=report.id)
|
|
||||||
add_audit_log(AuditSeverity.MODERATION, current_user, f"Reopened report \"{report.title}\"", url)
|
|
||||||
|
|
||||||
if report.thread:
|
|
||||||
add_replies(report.thread, current_user, f"Reopened report", is_status_update=True)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
else:
|
|
||||||
if "completed" in request.form:
|
|
||||||
outcome = "as completed"
|
|
||||||
elif "removed" in request.form:
|
|
||||||
outcome = "as content removed"
|
|
||||||
elif "invalid" in request.form:
|
|
||||||
outcome = "without action"
|
|
||||||
if report.thread:
|
|
||||||
flash("Make sure to comment why the report is invalid in the thread", "warning")
|
|
||||||
else:
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
report.is_resolved = True
|
|
||||||
url = url_for("report.view", rid=report.id)
|
|
||||||
add_audit_log(AuditSeverity.MODERATION, current_user, f"Report closed {outcome} \"{report.title}\"", url)
|
|
||||||
|
|
||||||
if report.thread:
|
|
||||||
add_replies(report.thread, current_user, f"Closed report {outcome}", is_status_update=True)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return render_template("report/view.html", report=report)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/admin/reports/<rid>/edit/", methods=["GET", "POST"])
|
|
||||||
def edit(rid: str):
|
|
||||||
report = Report.query.get_or_404(rid)
|
|
||||||
if not report.check_perm(current_user, Permission.SEE_REPORT):
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
form = ReportForm(request.form, obj=report)
|
|
||||||
form.submit.label.text = lazy_gettext("Save")
|
|
||||||
if form.validate_on_submit():
|
|
||||||
form.populate_obj(report)
|
|
||||||
url = url_for("report.view", rid=report.id)
|
|
||||||
add_audit_log(AuditSeverity.MODERATION, current_user, f"Edited report \"{report.title}\"", url)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return redirect(url_for("report.view", rid=report.id))
|
|
||||||
|
|
||||||
return render_template("report/edit.html", report=report, form=form)
|
|
||||||
|
|||||||
@@ -254,9 +254,6 @@ def view(id):
|
|||||||
if mentioned is None:
|
if mentioned is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not thread.check_perm(mentioned, Permission.SEE_THREAD):
|
|
||||||
continue
|
|
||||||
|
|
||||||
msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title)
|
msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title)
|
||||||
add_notification(mentioned, current_user, NotificationType.THREAD_REPLY,
|
add_notification(mentioned, current_user, NotificationType.THREAD_REPLY,
|
||||||
msg, thread.get_view_url(), thread.package)
|
msg, thread.get_view_url(), thread.package)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from flask_login import current_user, login_required
|
|||||||
from sqlalchemy import or_, and_
|
from sqlalchemy import or_, and_
|
||||||
|
|
||||||
from app.models import Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \
|
from app.models import Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \
|
||||||
PackageRelease, Permission, UserRank, License, MetaPackage, Dependency, AuditLogEntry, Tag, LuantiRelease, Report
|
PackageRelease, Permission, UserRank, License, MetaPackage, Dependency, AuditLogEntry, Tag, MinetestRelease
|
||||||
from app.querybuilder import QueryBuilder
|
from app.querybuilder import QueryBuilder
|
||||||
from app.utils import get_int_or_abort, is_yes, rank_required
|
from app.utils import get_int_or_abort, is_yes, rank_required
|
||||||
from . import bp
|
from . import bp
|
||||||
@@ -83,13 +83,11 @@ def view_editor():
|
|||||||
.order_by(db.desc(AuditLogEntry.created_at)) \
|
.order_by(db.desc(AuditLogEntry.created_at)) \
|
||||||
.limit(20).all()
|
.limit(20).all()
|
||||||
|
|
||||||
reports = Report.query.filter_by(is_resolved=False).order_by(db.asc(Report.created_at)).all() if current_user.rank.at_least(UserRank.EDITOR) else None
|
|
||||||
|
|
||||||
return render_template("todo/editor.html", current_tab="editor",
|
return render_template("todo/editor.html", current_tab="editor",
|
||||||
packages=packages, wip_packages=wip_packages, releases=releases, screenshots=screenshots,
|
packages=packages, wip_packages=wip_packages, releases=releases, screenshots=screenshots,
|
||||||
can_approve_new=can_approve_new, can_approve_rel=can_approve_rel, can_approve_scn=can_approve_scn,
|
can_approve_new=can_approve_new, can_approve_rel=can_approve_rel, can_approve_scn=can_approve_scn,
|
||||||
license_needed=license_needed, total_packages=total_packages, total_to_tag=total_to_tag,
|
license_needed=license_needed, total_packages=total_packages, total_to_tag=total_to_tag,
|
||||||
unfulfilled_meta_packages=unfulfilled_meta_packages, audit_log=audit_log, reports=reports)
|
unfulfilled_meta_packages=unfulfilled_meta_packages, audit_log=audit_log)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/todo/tags/")
|
@bp.route("/todo/tags/")
|
||||||
@@ -172,7 +170,7 @@ def screenshots():
|
|||||||
def mtver_support():
|
def mtver_support():
|
||||||
is_mtm_only = is_yes(request.args.get("mtm"))
|
is_mtm_only = is_yes(request.args.get("mtm"))
|
||||||
|
|
||||||
current_stable = LuantiRelease.query.filter(~LuantiRelease.name.like("%-dev")).order_by(db.desc(LuantiRelease.id)).first()
|
current_stable = MinetestRelease.query.filter(~MinetestRelease.name.like("%-dev")).order_by(db.desc(MinetestRelease.id)).first()
|
||||||
|
|
||||||
query = db.session.query(Package) \
|
query = db.session.query(Package) \
|
||||||
.filter(~Package.releases.any(or_(PackageRelease.max_rel==None, PackageRelease.max_rel == current_stable))) \
|
.filter(~Package.releases.any(or_(PackageRelease.max_rel==None, PackageRelease.max_rel == current_stable))) \
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ class RegisterForm(FlaskForm):
|
|||||||
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
|
email = StringField(lazy_gettext("Email"), [InputRequired(), Email()])
|
||||||
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(12, 100)])
|
password = PasswordField(lazy_gettext("Password"), [InputRequired(), Length(12, 100)])
|
||||||
question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()])
|
question = StringField(lazy_gettext("What is the result of the above calculation?"), [InputRequired()])
|
||||||
first_name = StringField("First name", [])
|
|
||||||
submit = SubmitField(lazy_gettext("Register"))
|
submit = SubmitField(lazy_gettext("Register"))
|
||||||
|
|
||||||
|
|
||||||
@@ -118,8 +117,6 @@ def handle_register(form):
|
|||||||
return user
|
return user
|
||||||
elif user is None:
|
elif user is None:
|
||||||
return
|
return
|
||||||
elif form.first_name.data != "":
|
|
||||||
abort(500)
|
|
||||||
|
|
||||||
user.password = make_flask_login_password(form.password.data)
|
user.password = make_flask_login_password(form.password.data)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from app.models import Package, APIToken, Permission, PackageState
|
|||||||
|
|
||||||
|
|
||||||
def get_packages_for_vcs_and_token(token: APIToken, repo_url: str) -> list[Package]:
|
def get_packages_for_vcs_and_token(token: APIToken, repo_url: str) -> list[Package]:
|
||||||
repo_url = repo_url.replace("https://", "").replace("http://", "").lower()
|
|
||||||
if token.package:
|
if token.package:
|
||||||
packages = [token.package]
|
packages = [token.package]
|
||||||
if not token.package.check_perm(token.owner, Permission.APPROVE_RELEASE):
|
if not token.package.check_perm(token.owner, Permission.APPROVE_RELEASE):
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def webhook_impl():
|
|||||||
if token is None:
|
if token is None:
|
||||||
return error(403, "Invalid authentication")
|
return error(403, "Invalid authentication")
|
||||||
|
|
||||||
packages = get_packages_for_vcs_and_token(token, json["project"]["web_url"])
|
packages = get_packages_for_vcs_and_token(token, json["project"]["web_url"].replace("https://", "").replace("http://", ""))
|
||||||
for package in packages:
|
for package in packages:
|
||||||
#
|
#
|
||||||
# Check event
|
# Check event
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from .models import User, UserRank, LuantiRelease, Tag, License, Notification, NotificationType, Package, \
|
from .models import User, UserRank, MinetestRelease, Tag, License, Notification, NotificationType, Package, \
|
||||||
PackageState, PackageType, PackageRelease, MetaPackage, Dependency
|
PackageState, PackageType, PackageRelease, MetaPackage, Dependency
|
||||||
from .utils import make_flask_login_password
|
from .utils import make_flask_login_password
|
||||||
|
|
||||||
@@ -35,12 +35,12 @@ def populate(session):
|
|||||||
system_user.rank = UserRank.BOT
|
system_user.rank = UserRank.BOT
|
||||||
session.add(system_user)
|
session.add(system_user)
|
||||||
|
|
||||||
session.add(LuantiRelease("None", 0))
|
session.add(MinetestRelease("None", 0))
|
||||||
session.add(LuantiRelease("0.4.16/17", 32))
|
session.add(MinetestRelease("0.4.16/17", 32))
|
||||||
session.add(LuantiRelease("5.0", 37))
|
session.add(MinetestRelease("5.0", 37))
|
||||||
session.add(LuantiRelease("5.1", 38))
|
session.add(MinetestRelease("5.1", 38))
|
||||||
session.add(LuantiRelease("5.2", 39))
|
session.add(MinetestRelease("5.2", 39))
|
||||||
session.add(LuantiRelease("5.3", 39))
|
session.add(MinetestRelease("5.3", 39))
|
||||||
|
|
||||||
tags = {}
|
tags = {}
|
||||||
for tag in ["Inventory", "Mapgen", "Building",
|
for tag in ["Inventory", "Mapgen", "Building",
|
||||||
@@ -69,8 +69,8 @@ def populate_test_data(session):
|
|||||||
licenses = { x.name : x for x in License.query.all() }
|
licenses = { x.name : x for x in License.query.all() }
|
||||||
tags = { x.name : x for x in Tag.query.all() }
|
tags = { x.name : x for x in Tag.query.all() }
|
||||||
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
|
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
|
||||||
v4 = LuantiRelease.query.filter_by(protocol=32).first()
|
v4 = MinetestRelease.query.filter_by(protocol=32).first()
|
||||||
v51 = LuantiRelease.query.filter_by(protocol=38).first()
|
v51 = MinetestRelease.query.filter_by(protocol=38).first()
|
||||||
|
|
||||||
ez = User("Shara")
|
ez = User("Shara")
|
||||||
ez.github_username = "Ezhh"
|
ez.github_username = "Ezhh"
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ as it was submitted as university coursework. To learn about the history and dev
|
|||||||
|
|
||||||
ContentDB is open source software, licensed under AGPLv3.0.
|
ContentDB is open source software, licensed under AGPLv3.0.
|
||||||
|
|
||||||
<a href="https://github.com/luanti-org/contentdb/" class="btn btn-primary me-1">Source code</a>
|
<a href="https://github.com/minetest/contentdb/" class="btn btn-primary me-1">Source code</a>
|
||||||
<a href="https://github.com/luanti-org/contentdb/issues/" class="btn btn-secondary me-1">Issue tracker</a>
|
<a href="https://github.com/minetest/contentdb/issues/" class="btn btn-secondary me-1">Issue tracker</a>
|
||||||
<a href="{{ admin_contact_url }}" class="btn btn-secondary me-1">Contact admin</a>
|
<a href="{{ admin_contact_url }}" class="btn btn-secondary me-1">Contact admin</a>
|
||||||
{% if monitoring_url -%}
|
{% if monitoring_url -%}
|
||||||
<a href="{{ monitoring_url }}" class="btn btn-secondary">Stats / monitoring</a>
|
<a href="{{ monitoring_url }}" class="btn btn-secondary">Stats / monitoring</a>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: API
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
* [How the Luanti client uses the API](https://github.com/luanti-org/contentdb/blob/master/docs/luanti_client.md)
|
* [How the Luanti client uses the API](https://github.com/minetest/contentdb/blob/master/docs/minetest_client.md)
|
||||||
|
|
||||||
|
|
||||||
## Responses and Error Handling
|
## Responses and Error Handling
|
||||||
@@ -131,7 +131,7 @@ curl -X DELETE https://content.luanti.org/api/delete-token/ \
|
|||||||
* `<neutral>` with a thumbs up icon.
|
* `<neutral>` with a thumbs up icon.
|
||||||
* `<thumbsdown>` with a thumbs up icon.
|
* `<thumbsdown>` with a thumbs up icon.
|
||||||
* GET `/api/packages/<author>/<name>/hypertext/`
|
* GET `/api/packages/<author>/<name>/hypertext/`
|
||||||
* Converts the long description to [Luanti Markup Language](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md#markup-language)
|
* Converts the long description to [Luanti Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
|
||||||
to be used in a `hypertext` formspec element.
|
to be used in a `hypertext` formspec element.
|
||||||
* Query arguments:
|
* Query arguments:
|
||||||
* `formspec_version`: Required, maximum supported formspec version.
|
* `formspec_version`: Required, maximum supported formspec version.
|
||||||
@@ -577,7 +577,7 @@ Supported query parameters:
|
|||||||
* Get JSON Schema of `.cdb.json`, including licenses, tags and content warnings.
|
* Get JSON Schema of `.cdb.json`, including licenses, tags and content warnings.
|
||||||
* See [JSON Schema Reference](https://json-schema.org/).
|
* See [JSON Schema Reference](https://json-schema.org/).
|
||||||
* POST `/api/hypertext/`
|
* POST `/api/hypertext/`
|
||||||
* Converts HTML or Markdown to [Luanti Markup Language](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md#markup-language)
|
* Converts HTML or Markdown to [Luanti Markup Language](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#markup-language)
|
||||||
to be used in a `hypertext` formspec element.
|
to be used in a `hypertext` formspec element.
|
||||||
* Post data: HTML or Markdown as plain text.
|
* Post data: HTML or Markdown as plain text.
|
||||||
* Content-Type: `text/html` or `text/markdown`.
|
* Content-Type: `text/html` or `text/markdown`.
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ can also translate your ContentDB page. See Edit Package > Translation for more
|
|||||||
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html">
|
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html">
|
||||||
{{ _("Translation - Luanti Modding Book") }}
|
{{ _("Translation - Luanti Modding Book") }}
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-primary" href="https://api.luanti.org/translations/#translating-content-meta">
|
<a class="btn btn-primary" href="https://api.minetest.net/translations/#translating-content-meta">
|
||||||
{{ _("Translating content meta - lua_api.md") }}
|
{{ _("Translating content meta - lua_api.md") }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ permanent bans.
|
|||||||
|
|
||||||
## Where can I get help?
|
## Where can I get help?
|
||||||
|
|
||||||
[Join](https://www.luanti.org/get-involved/) IRC, Matrix, or Discord to ask for help.
|
[Join](https://www.minetest.net/get-involved/) IRC, Matrix, or Discord to ask for help.
|
||||||
In Discord, there are the #assets or #contentdb channels. In IRC or Matrix, you can just ask in the main channels.
|
In Discord, there are the #assets or #contentdb channels. In IRC or Matrix, you can just ask in the main channels.
|
||||||
|
|
||||||
If your package is already on ContentDB, you can open a thread.
|
If your package is already on ContentDB, you can open a thread.
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ downloaded from that IP.
|
|||||||
You can see all scores using the [scores REST API](/api/scores/), or by
|
You can see all scores using the [scores REST API](/api/scores/), or by
|
||||||
using the [Prometheus metrics](/help/metrics/) endpoint.
|
using the [Prometheus metrics](/help/metrics/) endpoint.
|
||||||
|
|
||||||
Consider [suggesting improvements](https://github.com/luanti-org/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).
|
Consider [suggesting improvements](https://github.com/minetest/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ title: Privacy Policy
|
|||||||
---
|
---
|
||||||
|
|
||||||
Last Updated: 2024-04-30
|
Last Updated: 2024-04-30
|
||||||
([View updates](https://github.com/luanti-org/contentdb/commits/master/app/flatpages/privacy_policy.md))
|
([View updates](https://github.com/minetest/contentdb/commits/master/app/flatpages/privacy_policy.md))
|
||||||
|
|
||||||
## What Information is Collected
|
## What Information is Collected
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ def _get_approval_statistics(entries: list[AuditLogEntry], start_date: Optional[
|
|||||||
(end_date is None or entry.created_at <= end_date)))
|
(end_date is None or entry.created_at <= end_date)))
|
||||||
info.is_in_range = info.is_in_range or is_in_range
|
info.is_in_range = info.is_in_range or is_in_range
|
||||||
|
|
||||||
new_state = get_state(entry.title.replace("…", "") + (entry.description or ""))
|
new_state = get_state(entry.title)
|
||||||
if new_state == info.state:
|
if new_state == info.state:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,12 @@ class GameSupport:
|
|||||||
|
|
||||||
def _get_supported_games(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
|
def _get_supported_games(self, package: GSPackage, visited: list[str]) -> Optional[set[str]]:
|
||||||
if package.id_ in visited:
|
if package.id_ in visited:
|
||||||
|
# first_idx = visited.index(package.id_)
|
||||||
|
# visited = visited[first_idx:]
|
||||||
|
# err = f"Dependency cycle detected: {' -> '.join(visited)} -> {package.id_}"
|
||||||
|
# for id_ in visited:
|
||||||
|
# package2 = self.get(id_)
|
||||||
|
# package2.add_error(err)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if package.type == PackageType.GAME:
|
if package.type == PackageType.GAME:
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ class PackageValidationNote:
|
|||||||
self.buttons.append((url, label))
|
self.buttons.append((url, label))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return str(self.message)
|
|
||||||
|
|
||||||
|
|
||||||
def is_package_name_taken(normalised_name: str) -> bool:
|
def is_package_name_taken(normalised_name: str) -> bool:
|
||||||
return Package.query.filter(
|
return Package.query.filter(
|
||||||
@@ -110,7 +107,8 @@ def validate_package_for_approval(package: Package) -> List[PackageValidationNot
|
|||||||
# Don't bother validating any more until we have a release
|
# Don't bother validating any more until we have a release
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
if package.screenshots.count() == 0:
|
if (package.type == PackageType.GAME or package.type == PackageType.TXP) and \
|
||||||
|
package.screenshots.count() == 0:
|
||||||
danger(lazy_gettext("You need to add at least one screenshot."))
|
danger(lazy_gettext("You need to add at least one screenshot."))
|
||||||
|
|
||||||
missing_deps = package.get_missing_hard_dependencies_query().all()
|
missing_deps = package.get_missing_hard_dependencies_query().all()
|
||||||
|
|||||||
@@ -69,19 +69,6 @@ ALLOWED_FIELDS = {
|
|||||||
"translation_url": str,
|
"translation_url": str,
|
||||||
}
|
}
|
||||||
|
|
||||||
NULLABLE = {
|
|
||||||
"tags",
|
|
||||||
"content_warnings",
|
|
||||||
"repo",
|
|
||||||
"website",
|
|
||||||
"issue_tracker",
|
|
||||||
"issueTracker",
|
|
||||||
"forums",
|
|
||||||
"video_url",
|
|
||||||
"donate_url",
|
|
||||||
"translation_url",
|
|
||||||
}
|
|
||||||
|
|
||||||
ALIASES = {
|
ALIASES = {
|
||||||
"short_description": "short_desc",
|
"short_description": "short_desc",
|
||||||
"issue_tracker": "issueTracker",
|
"issue_tracker": "issueTracker",
|
||||||
@@ -99,13 +86,11 @@ def is_int(val):
|
|||||||
|
|
||||||
def validate(data: dict):
|
def validate(data: dict):
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if value is None:
|
if value is not None:
|
||||||
check(key in NULLABLE, f"{key} must not be null")
|
|
||||||
else:
|
|
||||||
typ = ALLOWED_FIELDS.get(key)
|
typ = ALLOWED_FIELDS.get(key)
|
||||||
check(typ is not None, f"{key} is not a known field")
|
check(typ is not None, key + " is not a known field")
|
||||||
if typ != AnyType:
|
if typ != AnyType:
|
||||||
check(isinstance(value, typ), f"{key} must be a " + typ.__name__)
|
check(isinstance(value, typ), key + " must be a " + typ.__name__)
|
||||||
|
|
||||||
if "name" in data:
|
if "name" in data:
|
||||||
name = data["name"]
|
name = data["name"]
|
||||||
@@ -117,8 +102,8 @@ def validate(data: dict):
|
|||||||
value = data.get(key)
|
value = data.get(key)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
check(value.startswith("http://") or value.startswith("https://"),
|
check(value.startswith("http://") or value.startswith("https://"),
|
||||||
f"{key} must start with http:// or https://")
|
key + " must start with http:// or https://")
|
||||||
check(validators.url(value), f"{key} must be a valid URL")
|
check(validators.url(value), key + " must be a valid URL")
|
||||||
|
|
||||||
|
|
||||||
def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool, data: dict,
|
def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool, data: dict,
|
||||||
@@ -136,9 +121,6 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
|||||||
|
|
||||||
for alias, to in ALIASES.items():
|
for alias, to in ALIASES.items():
|
||||||
if alias in data:
|
if alias in data:
|
||||||
if to in data and data[to] != data[alias]:
|
|
||||||
raise LogicError(403, f"Aliased field ({alias}) does not match new field ({to})")
|
|
||||||
|
|
||||||
data[to] = data[alias]
|
data[to] = data[alias]
|
||||||
|
|
||||||
validate(data)
|
validate(data)
|
||||||
@@ -187,6 +169,7 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
|||||||
package.provides.append(m)
|
package.provides.append(m)
|
||||||
|
|
||||||
if "tags" in data:
|
if "tags" in data:
|
||||||
|
old_tags = list(package.tags)
|
||||||
package.tags.clear()
|
package.tags.clear()
|
||||||
for tag_id in (data["tags"] or []):
|
for tag_id in (data["tags"] or []):
|
||||||
if is_int(tag_id):
|
if is_int(tag_id):
|
||||||
@@ -210,10 +193,7 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
|
|||||||
package.content_warnings.append(warning)
|
package.content_warnings.append(warning)
|
||||||
|
|
||||||
was_modified = was_new
|
was_modified = was_new
|
||||||
if was_new:
|
if not was_new:
|
||||||
msg = f"Created package {package.author.username}/{package.name}"
|
|
||||||
add_audit_log(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package)
|
|
||||||
else:
|
|
||||||
after_dict = package.as_dict("/")
|
after_dict = package.as_dict("/")
|
||||||
diff = diff_dictionaries(before_dict, after_dict)
|
diff = diff_dictionaries(before_dict, after_dict)
|
||||||
was_modified = len(diff) > 0
|
was_modified = len(diff) > 0
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from flask_babel import lazy_gettext
|
|||||||
|
|
||||||
from app.logic.LogicError import LogicError
|
from app.logic.LogicError import LogicError
|
||||||
from app.logic.uploads import upload_file
|
from app.logic.uploads import upload_file
|
||||||
from app.models import PackageRelease, db, Permission, User, Package, LuantiRelease
|
from app.models import PackageRelease, db, Permission, User, Package, MinetestRelease
|
||||||
from app.tasks.importtasks import make_vcs_release, check_zip_release
|
from app.tasks.importtasks import make_vcs_release, check_zip_release
|
||||||
from app.utils import AuditSeverity, add_audit_log, nonempty_or_none, normalize_line_endings
|
from app.utils import AuditSeverity, add_audit_log, nonempty_or_none, normalize_line_endings
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ def check_can_create_release(user: User, package: Package, name: str):
|
|||||||
|
|
||||||
|
|
||||||
def do_create_vcs_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
|
def do_create_vcs_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str,
|
||||||
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason: str = None):
|
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None):
|
||||||
check_can_create_release(user, package, name)
|
check_can_create_release(user, package, name)
|
||||||
|
|
||||||
rel = PackageRelease()
|
rel = PackageRelease()
|
||||||
@@ -70,7 +70,7 @@ def do_create_vcs_release(user: User, package: Package, name: str, title: Option
|
|||||||
|
|
||||||
|
|
||||||
def do_create_zip_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
|
def do_create_zip_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file,
|
||||||
min_v: LuantiRelease = None, max_v: LuantiRelease = None, reason: str = None,
|
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None,
|
||||||
commit_hash: str = None):
|
commit_hash: str = None):
|
||||||
check_can_create_release(user, package, name)
|
check_can_create_release(user, package, name)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import imghdr
|
import imghdr
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask_babel import lazy_gettext, LazyString
|
from flask_babel import lazy_gettext
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from app.logic.LogicError import LogicError
|
from app.logic.LogicError import LogicError
|
||||||
@@ -35,7 +35,7 @@ def is_allowed_image(data):
|
|||||||
return imghdr.what(None, data) in ALLOWED_IMAGES
|
return imghdr.what(None, data) in ALLOWED_IMAGES
|
||||||
|
|
||||||
|
|
||||||
def upload_file(file, file_type: str, file_type_desc: LazyString | str, length: int=10):
|
def upload_file(file, file_type, file_type_desc):
|
||||||
if not file or file is None or file.filename == "":
|
if not file or file is None or file.filename == "":
|
||||||
raise LogicError(400, "Expected file")
|
raise LogicError(400, "Expected file")
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ def upload_file(file, file_type: str, file_type_desc: LazyString | str, length:
|
|||||||
|
|
||||||
file.stream.seek(0)
|
file.stream.seek(0)
|
||||||
|
|
||||||
filename = random_string(length) + "." + ext
|
filename = random_string(10) + "." + ext
|
||||||
filepath = os.path.join(app.config["UPLOAD_DIR"], filename)
|
filepath = os.path.join(app.config["UPLOAD_DIR"], filename)
|
||||||
file.save(filepath)
|
file.save(filepath)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
from urllib.parse import urljoin
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from jinja2.utils import markupsafe
|
from jinja2.utils import markupsafe
|
||||||
from markdown_it import MarkdownIt
|
from markdown_it import MarkdownIt
|
||||||
@@ -57,7 +58,7 @@ def render_code(self, tokens: Sequence[Token], idx, options, env):
|
|||||||
|
|
||||||
gfm_like.make()
|
gfm_like.make()
|
||||||
md = MarkdownIt("gfm-like", {"highlight": highlight_code})
|
md = MarkdownIt("gfm-like", {"highlight": highlight_code})
|
||||||
md.use(anchors_plugin, permalink=True, permalinkSymbol="🔗", max_level=6)
|
md.use(anchors_plugin, permalink=True, permalinkSymbol="#", max_level=6)
|
||||||
md.add_render_rule("fence", render_code)
|
md.add_render_rule("fence", render_code)
|
||||||
init_mention(md)
|
init_mention(md)
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ def get_user_mentions(html: str) -> set:
|
|||||||
return set([x.get("data-username") for x in links])
|
return set([x.get("data-username") for x in links])
|
||||||
|
|
||||||
|
|
||||||
def get_links(html: str) -> set:
|
def get_links(html: str, url: str) -> set:
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
links = soup.select("a[href]")
|
links = soup.select("a[href]")
|
||||||
return set([x.get("href") for x in links])
|
return set([urljoin(url, x.get("href")) for x in links])
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from flask_babel import LazyString
|
|
||||||
|
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from sqlalchemy_searchable import make_searchable
|
from sqlalchemy_searchable import make_searchable
|
||||||
@@ -124,115 +125,13 @@ class AuditLogEntry(db.Model):
|
|||||||
raise Exception("Unknown permission given to AuditLogEntry.check_perm()")
|
raise Exception("Unknown permission given to AuditLogEntry.check_perm()")
|
||||||
|
|
||||||
if perm == Permission.VIEW_AUDIT_DESCRIPTION:
|
if perm == Permission.VIEW_AUDIT_DESCRIPTION:
|
||||||
return (self.package and user in self.package.maintainers) or user.rank.at_least(UserRank.APPROVER if self.package is not None else UserRank.MODERATOR)
|
return user.rank.at_least(UserRank.APPROVER if self.package is not None else UserRank.MODERATOR)
|
||||||
else:
|
else:
|
||||||
raise Exception("Permission {} is not related to audit log entries".format(perm.name))
|
raise Exception("Permission {} is not related to audit log entries".format(perm.name))
|
||||||
|
|
||||||
|
|
||||||
class ReportCategory(enum.Enum):
|
|
||||||
ACCOUNT_DELETION = "account_deletion"
|
|
||||||
COPYRIGHT = "copyright"
|
|
||||||
USER_CONDUCT = "user_conduct"
|
|
||||||
SPAM = "spam"
|
|
||||||
ILLEGAL_HARMFUL = "illegal_harmful"
|
|
||||||
REVIEW = "review"
|
|
||||||
APPEAL = "appeal"
|
|
||||||
OTHER = "other"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def title(self) -> LazyString:
|
|
||||||
if self == ReportCategory.ACCOUNT_DELETION:
|
|
||||||
return lazy_gettext("Account deletion")
|
|
||||||
elif self == ReportCategory.COPYRIGHT:
|
|
||||||
return lazy_gettext("Copyright infringement / DMCA")
|
|
||||||
elif self == ReportCategory.USER_CONDUCT:
|
|
||||||
return lazy_gettext("User behaviour, bullying, or abuse")
|
|
||||||
elif self == ReportCategory.SPAM:
|
|
||||||
return lazy_gettext("Spam")
|
|
||||||
elif self == ReportCategory.ILLEGAL_HARMFUL:
|
|
||||||
return lazy_gettext("Illegal or harmful content")
|
|
||||||
elif self == ReportCategory.REVIEW:
|
|
||||||
return lazy_gettext("Outdated/invalid review")
|
|
||||||
elif self == ReportCategory.APPEAL:
|
|
||||||
return lazy_gettext("Appeal")
|
|
||||||
elif self == ReportCategory.OTHER:
|
|
||||||
return lazy_gettext("Other")
|
|
||||||
else:
|
|
||||||
raise Exception("Unknown report category")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, name):
|
|
||||||
try:
|
|
||||||
return ReportCategory[name.upper()]
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def choices(cls, with_none):
|
|
||||||
ret = [(choice, choice.title) for choice in cls]
|
|
||||||
|
|
||||||
if with_none:
|
|
||||||
ret.insert(0, (None, ""))
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def coerce(cls, item):
|
|
||||||
if item is None or (isinstance(item, str) and item.upper() == "NONE"):
|
|
||||||
return None
|
|
||||||
return item if type(item) == ReportCategory else ReportCategory[item.upper()]
|
|
||||||
|
|
||||||
|
|
||||||
class Report(db.Model):
|
|
||||||
id = db.Column(db.String(24), primary_key=True)
|
|
||||||
|
|
||||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
|
||||||
|
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
|
|
||||||
user = db.relationship("User", foreign_keys=[user_id], back_populates="reports")
|
|
||||||
|
|
||||||
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True)
|
|
||||||
thread = db.relationship("Thread", foreign_keys=[thread_id])
|
|
||||||
|
|
||||||
category = db.Column(db.Enum(ReportCategory), nullable=False)
|
|
||||||
url = db.Column(db.String, nullable=True)
|
|
||||||
title = db.Column(db.Unicode(300), nullable=False)
|
|
||||||
message = db.Column(db.UnicodeText, nullable=False)
|
|
||||||
|
|
||||||
is_resolved = db.Column(db.Boolean, nullable=False, default=False)
|
|
||||||
|
|
||||||
attachments = db.relationship("ReportAttachment", back_populates="report", lazy="dynamic", cascade="all, delete, delete-orphan")
|
|
||||||
|
|
||||||
def check_perm(self, user, perm):
|
|
||||||
if type(perm) == str:
|
|
||||||
perm = Permission[perm]
|
|
||||||
elif type(perm) != Permission:
|
|
||||||
raise Exception("Unknown permission given to Report.check_perm()")
|
|
||||||
if not user.is_authenticated:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if perm == Permission.SEE_REPORT:
|
|
||||||
return user.rank.at_least(UserRank.EDITOR)
|
|
||||||
else:
|
|
||||||
raise Exception("Permission {} is not related to reports".format(perm.name))
|
|
||||||
|
|
||||||
|
|
||||||
class ReportAttachment(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
|
|
||||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
|
||||||
|
|
||||||
report_id = db.Column(db.String(24), db.ForeignKey("report.id"), nullable=False)
|
|
||||||
report = db.relationship("Report", foreign_keys=[report_id], back_populates="attachments")
|
|
||||||
|
|
||||||
url = db.Column(db.String(100), nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com",
|
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com",
|
||||||
"minetest.net", "luanti.org", "dropboxusercontent.com", "4shared.com",
|
"minetest.net", "dropboxusercontent.com", "4shared.com",
|
||||||
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net",
|
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net",
|
||||||
"imageshack.com", "imgur.com"]
|
"imageshack.com", "imgur.com"]
|
||||||
|
|
||||||
|
|||||||
@@ -1043,7 +1043,7 @@ class Tag(db.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LuantiRelease(db.Model):
|
class MinetestRelease(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
protocol = db.Column(db.Integer, nullable=False, default=0)
|
protocol = db.Column(db.Integer, nullable=False, default=0)
|
||||||
@@ -1067,11 +1067,11 @@ class LuantiRelease(db.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, version: typing.Optional[str], protocol_num: typing.Optional[str]) -> typing.Optional["LuantiRelease"]:
|
def get(cls, version: typing.Optional[str], protocol_num: typing.Optional[str]) -> typing.Optional["MinetestRelease"]:
|
||||||
if version:
|
if version:
|
||||||
parts = version.strip().split(".")
|
parts = version.strip().split(".")
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
query = LuantiRelease.query.filter(func.replace(LuantiRelease.name, "-dev", "") == "{}.{}".format(parts[0], parts[1]))
|
query = MinetestRelease.query.filter(func.replace(MinetestRelease.name, "-dev", "") == "{}.{}".format(parts[0], parts[1]))
|
||||||
if protocol_num:
|
if protocol_num:
|
||||||
query = query.filter_by(protocol=protocol_num)
|
query = query.filter_by(protocol=protocol_num)
|
||||||
|
|
||||||
@@ -1081,9 +1081,9 @@ class LuantiRelease(db.Model):
|
|||||||
|
|
||||||
if protocol_num:
|
if protocol_num:
|
||||||
# Find the closest matching release
|
# Find the closest matching release
|
||||||
return LuantiRelease.query.order_by(db.desc(LuantiRelease.protocol),
|
return MinetestRelease.query.order_by(db.desc(MinetestRelease.protocol),
|
||||||
db.desc(LuantiRelease.id)) \
|
db.desc(MinetestRelease.id)) \
|
||||||
.filter(LuantiRelease.protocol <= protocol_num).first()
|
.filter(MinetestRelease.protocol <= protocol_num).first()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1103,7 +1103,6 @@ class PackageRelease(db.Model):
|
|||||||
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
||||||
downloads = db.Column(db.Integer, nullable=False, default=0)
|
downloads = db.Column(db.Integer, nullable=False, default=0)
|
||||||
release_notes = db.Column(db.UnicodeText, nullable=True, default=None)
|
release_notes = db.Column(db.UnicodeText, nullable=True, default=None)
|
||||||
file_size_bytes = db.Column(db.Integer, nullable=False, default=0)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def summary(self) -> str:
|
def summary(self) -> str:
|
||||||
@@ -1114,11 +1113,11 @@ class PackageRelease(db.Model):
|
|||||||
|
|
||||||
return self.release_notes.split("\n")[0]
|
return self.release_notes.split("\n")[0]
|
||||||
|
|
||||||
min_rel_id = db.Column(db.Integer, db.ForeignKey("luanti_release.id"), nullable=True, server_default=None)
|
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||||
min_rel = db.relationship("LuantiRelease", foreign_keys=[min_rel_id])
|
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
|
||||||
|
|
||||||
max_rel_id = db.Column(db.Integer, db.ForeignKey("luanti_release.id"), nullable=True, server_default=None)
|
max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||||
max_rel = db.relationship("LuantiRelease", foreign_keys=[max_rel_id])
|
max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
|
||||||
|
|
||||||
# If the release is approved, then the task_id must be null and the url must be present
|
# If the release is approved, then the task_id must be null and the url must be present
|
||||||
CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
|
CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
|
||||||
@@ -1127,14 +1126,14 @@ class PackageRelease(db.Model):
|
|||||||
def file_path(self):
|
def file_path(self):
|
||||||
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
||||||
|
|
||||||
def calculate_file_size_bytes(self):
|
@property
|
||||||
|
def file_size_bytes(self):
|
||||||
path = self.file_path
|
path = self.file_path
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
self.file_size_bytes = 0
|
return 0
|
||||||
return
|
|
||||||
|
|
||||||
file_stats = os.stat(path)
|
file_stats = os.stat(path)
|
||||||
self.file_size_bytes = file_stats.st_size
|
return file_stats.st_size
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_size(self):
|
def file_size(self):
|
||||||
@@ -1264,8 +1263,6 @@ class PackageScreenshot(db.Model):
|
|||||||
width = db.Column(db.Integer, nullable=False)
|
width = db.Column(db.Integer, nullable=False)
|
||||||
height = db.Column(db.Integer, nullable=False)
|
height = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
file_size_bytes = db.Column(db.Integer, nullable=False, default=0)
|
|
||||||
|
|
||||||
def is_very_small(self):
|
def is_very_small(self):
|
||||||
return self.width < 720 or self.height < 405
|
return self.width < 720 or self.height < 405
|
||||||
|
|
||||||
@@ -1279,14 +1276,14 @@ class PackageScreenshot(db.Model):
|
|||||||
def file_path(self):
|
def file_path(self):
|
||||||
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
return self.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
||||||
|
|
||||||
def calculate_file_size_bytes(self):
|
@property
|
||||||
|
def file_size_bytes(self):
|
||||||
path = self.file_path
|
path = self.file_path
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
self.file_size_bytes = 0
|
return 0
|
||||||
return
|
|
||||||
|
|
||||||
file_stats = os.stat(path)
|
file_stats = os.stat(path)
|
||||||
self.file_size_bytes = file_stats.st_size
|
return file_stats.st_size
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_size(self):
|
def file_size(self):
|
||||||
@@ -1371,8 +1368,6 @@ class PackageUpdateConfig(db.Model):
|
|||||||
# Set to now when an outdated notification is sent. Set to None when a release is created
|
# Set to now when an outdated notification is sent. Set to None when a release is created
|
||||||
outdated_at = db.Column(db.DateTime, nullable=True, default=None)
|
outdated_at = db.Column(db.DateTime, nullable=True, default=None)
|
||||||
|
|
||||||
last_checked_at = db.Column(db.DateTime, nullable=True, default=None)
|
|
||||||
|
|
||||||
trigger = db.Column(db.Enum(PackageUpdateTrigger), nullable=False, default=PackageUpdateTrigger.COMMIT)
|
trigger = db.Column(db.Enum(PackageUpdateTrigger), nullable=False, default=PackageUpdateTrigger.COMMIT)
|
||||||
ref = db.Column(db.String(41), nullable=True, default=None)
|
ref = db.Column(db.String(41), nullable=True, default=None)
|
||||||
|
|
||||||
@@ -1443,7 +1438,7 @@ class PackageDailyStats(db.Model):
|
|||||||
reason_update = db.Column(db.Integer, nullable=False, default=0)
|
reason_update = db.Column(db.Integer, nullable=False, default=0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update(package: Package, is_luanti: bool, reason: str):
|
def update(package: Package, is_minetest: bool, reason: str):
|
||||||
date = datetime.datetime.utcnow().date()
|
date = datetime.datetime.utcnow().date()
|
||||||
|
|
||||||
to_update = dict()
|
to_update = dict()
|
||||||
@@ -1451,7 +1446,7 @@ class PackageDailyStats(db.Model):
|
|||||||
"package_id": package.id, "date": date
|
"package_id": package.id, "date": date
|
||||||
}
|
}
|
||||||
|
|
||||||
field_platform = "platform_minetest" if is_luanti else "platform_other"
|
field_platform = "platform_minetest" if is_minetest else "platform_other"
|
||||||
to_update[field_platform] = getattr(PackageDailyStats, field_platform) + 1
|
to_update[field_platform] = getattr(PackageDailyStats, field_platform) + 1
|
||||||
kwargs[field_platform] = 1
|
kwargs[field_platform] = 1
|
||||||
|
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ class Thread(db.Model):
|
|||||||
|
|
||||||
watchers = db.relationship("User", secondary=watchers, backref="watching")
|
watchers = db.relationship("User", secondary=watchers, backref="watching")
|
||||||
|
|
||||||
report = db.relationship("Report", foreign_keys="Report.thread_id", back_populates="thread", lazy="dynamic")
|
|
||||||
|
|
||||||
first_reply = db.relationship("ThreadReply", uselist=False, foreign_keys="ThreadReply.thread_id",
|
first_reply = db.relationship("ThreadReply", uselist=False, foreign_keys="ThreadReply.thread_id",
|
||||||
lazy=True, order_by=db.asc("id"), viewonly=True,
|
lazy=True, order_by=db.asc("id"), viewonly=True,
|
||||||
primaryjoin="Thread.id==ThreadReply.thread_id")
|
primaryjoin="Thread.id==ThreadReply.thread_id")
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ class Permission(enum.Enum):
|
|||||||
CHANGE_USERNAMES = "CHANGE_USERNAMES"
|
CHANGE_USERNAMES = "CHANGE_USERNAMES"
|
||||||
CHANGE_RANK = "CHANGE_RANK"
|
CHANGE_RANK = "CHANGE_RANK"
|
||||||
CHANGE_EMAIL = "CHANGE_EMAIL"
|
CHANGE_EMAIL = "CHANGE_EMAIL"
|
||||||
LINK_TO_WEBSITE = "LINK_TO_WEBSITE"
|
|
||||||
SEE_THREAD = "SEE_THREAD"
|
SEE_THREAD = "SEE_THREAD"
|
||||||
CREATE_THREAD = "CREATE_THREAD"
|
CREATE_THREAD = "CREATE_THREAD"
|
||||||
COMMENT_THREAD = "COMMENT_THREAD"
|
COMMENT_THREAD = "COMMENT_THREAD"
|
||||||
@@ -115,7 +114,6 @@ class Permission(enum.Enum):
|
|||||||
EDIT_COLLECTION = "EDIT_COLLECTION"
|
EDIT_COLLECTION = "EDIT_COLLECTION"
|
||||||
VIEW_COLLECTION = "VIEW_COLLECTION"
|
VIEW_COLLECTION = "VIEW_COLLECTION"
|
||||||
CREATE_OAUTH_CLIENT = "CREATE_OAUTH_CLIENT"
|
CREATE_OAUTH_CLIENT = "CREATE_OAUTH_CLIENT"
|
||||||
SEE_REPORT = "SEE_REPORT"
|
|
||||||
|
|
||||||
# Only return true if the permission is valid for *all* contexts
|
# Only return true if the permission is valid for *all* contexts
|
||||||
# See Package.check_perm for package-specific contexts
|
# See Package.check_perm for package-specific contexts
|
||||||
@@ -212,7 +210,6 @@ class User(db.Model, UserMixin):
|
|||||||
forum_topics = db.relationship("ForumTopic", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
forum_topics = db.relationship("ForumTopic", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||||
collections = db.relationship("Collection", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan", order_by=db.asc("title"))
|
collections = db.relationship("Collection", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan", order_by=db.asc("title"))
|
||||||
clients = db.relationship("OAuthClient", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
|
clients = db.relationship("OAuthClient", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||||
reports = db.relationship("Report", back_populates="user", lazy="dynamic", cascade="all")
|
|
||||||
|
|
||||||
ban = db.relationship("UserBan", foreign_keys="UserBan.user_id", back_populates="user", uselist=False)
|
ban = db.relationship("UserBan", foreign_keys="UserBan.user_id", back_populates="user", uselist=False)
|
||||||
|
|
||||||
@@ -290,8 +287,6 @@ class User(db.Model, UserMixin):
|
|||||||
return user.rank.at_least(UserRank.NEW_MEMBER)
|
return user.rank.at_least(UserRank.NEW_MEMBER)
|
||||||
else:
|
else:
|
||||||
return user.rank.at_least(UserRank.MODERATOR) and user.rank.at_least(self.rank)
|
return user.rank.at_least(UserRank.MODERATOR) and user.rank.at_least(self.rank)
|
||||||
elif perm == Permission.LINK_TO_WEBSITE:
|
|
||||||
return user.rank.at_least(UserRank.MEMBER)
|
|
||||||
else:
|
else:
|
||||||
raise Exception("Permission {} is not related to users".format(perm.name))
|
raise Exception("Permission {} is not related to users".format(perm.name))
|
||||||
|
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "v1.0.0",
|
|
||||||
"entity": {
|
|
||||||
"type": "organisation",
|
|
||||||
"role": "maintainer",
|
|
||||||
"name": "Luanti",
|
|
||||||
"email": "rw@rubenwardy.com",
|
|
||||||
"description": "Luanti (formerly Minetest) is an open-source voxel game creation platform",
|
|
||||||
"webpageUrl": {
|
|
||||||
"url": "https://www.luanti.org"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"projects": [
|
|
||||||
{
|
|
||||||
"guid": "luanti",
|
|
||||||
"name": "Luanti",
|
|
||||||
"description": "Luanti (formerly Minetest) is an open-source voxel game creation platform",
|
|
||||||
"webpageUrl": {
|
|
||||||
"url": "https://www.luanti.org"
|
|
||||||
},
|
|
||||||
"repositoryUrl": {
|
|
||||||
"url": "https://github.com/luanti-org/luanti"
|
|
||||||
},
|
|
||||||
"licenses": [
|
|
||||||
"spdx:LGPL-2.1",
|
|
||||||
"spdx:CC-BY-SA-3.0",
|
|
||||||
"spdx:MIT",
|
|
||||||
"spdx:Apache-2.0"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"lua",
|
|
||||||
"voxel",
|
|
||||||
"game"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "contentdb",
|
|
||||||
"name": "Luanti ContentDB",
|
|
||||||
"description": "A content database for Luanti mods, games, and more.",
|
|
||||||
"webpageUrl": {
|
|
||||||
"url": "https://content.luanti.org/about/"
|
|
||||||
},
|
|
||||||
"repositoryUrl": {
|
|
||||||
"url": "https://github.com/luanti-org/contentdb"
|
|
||||||
},
|
|
||||||
"licenses": [
|
|
||||||
"spdx:AGPL-3.0",
|
|
||||||
"spdx:CC-BY-SA-4.0"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"python",
|
|
||||||
"flask",
|
|
||||||
"luanti",
|
|
||||||
"minetest"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"channels": [
|
|
||||||
{
|
|
||||||
"guid": "open-collective",
|
|
||||||
"type": "other",
|
|
||||||
"address": "https://opencollective.com/luanti",
|
|
||||||
"description": "Recurring and one-time donations to Luanti"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"plans": [
|
|
||||||
{
|
|
||||||
"guid": "oc-eur-backer",
|
|
||||||
"status": "active",
|
|
||||||
"name": "Luanti backer",
|
|
||||||
"description": "Become a backer for €5 per month and help Luanti development",
|
|
||||||
"amount": 5,
|
|
||||||
"currency": "EUR",
|
|
||||||
"frequency": "monthly",
|
|
||||||
"channels": [
|
|
||||||
"open-collective"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "oc-eur-supporter",
|
|
||||||
"status": "active",
|
|
||||||
"name": "Luanti supporter",
|
|
||||||
"description": "Become a supporter for €5 per month and help Luanti development",
|
|
||||||
"amount": 100,
|
|
||||||
"currency": "EUR",
|
|
||||||
"frequency": "monthly",
|
|
||||||
"channels": [
|
|
||||||
"open-collective"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "oc-eur-custom",
|
|
||||||
"status": "active",
|
|
||||||
"name": "Luanti custom one-off",
|
|
||||||
"description": "You may donate any amount you're comfortable with",
|
|
||||||
"amount": 0,
|
|
||||||
"currency": "EUR",
|
|
||||||
"frequency": "one-time",
|
|
||||||
"channels": [
|
|
||||||
"open-collective"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"guid": "fosdem",
|
|
||||||
"status": "active",
|
|
||||||
"name": "FOSDEM",
|
|
||||||
"description": "It costs us €3000 to attend FOSDEM",
|
|
||||||
"amount": 3000,
|
|
||||||
"currency": "EUR",
|
|
||||||
"frequency": "one-time",
|
|
||||||
"channels": [
|
|
||||||
"open-collective"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"history": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -68,9 +68,8 @@ window.addEventListener("load", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupHints("short_desc", {
|
setupHints("short_desc", {
|
||||||
"short_desc_mods": (val) => val.indexOf("luanti") >= 0 || val.indexOf("minetest") >= 0 ||
|
"short_desc_mods": (val) => val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
|
||||||
val.indexOf("mod") >= 0 || val.indexOf("modpack") >= 0 ||
|
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0,
|
||||||
val.indexOf("mod pack") >= 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setupHints("desc", {
|
setupHints("desc", {
|
||||||
@@ -86,8 +85,7 @@ window.addEventListener("load", () => {
|
|||||||
"desc_page_topic": (val) => {
|
"desc_page_topic": (val) => {
|
||||||
const topicId = document.getElementById("forums").value;
|
const topicId = document.getElementById("forums").value;
|
||||||
const r = new RegExp(`forum\\.minetest\\.net\\/viewtopic\\.php\\?[a-z0-9=&]*t=${topicId}`);
|
const r = new RegExp(`forum\\.minetest\\.net\\/viewtopic\\.php\\?[a-z0-9=&]*t=${topicId}`);
|
||||||
const r2 = new RegExp(`forum\\.luanti\\.org\\/viewtopic\\.php\\?[a-z0-9=&]*t=${topicId}`);
|
return topicId && r.test(val);
|
||||||
return topicId && (r.test(val) || r2.test(val));
|
|
||||||
},
|
},
|
||||||
"desc_page_repo": (val) => {
|
"desc_page_repo": (val) => {
|
||||||
const repoUrl = document.getElementById("repo").value.replace(".git", "");
|
const repoUrl = document.getElementById("repo").value.replace(".git", "");
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ window.addEventListener("load", () => {
|
|||||||
bar.setAttribute("aria-valuenow", current);
|
bar.setAttribute("aria-valuenow", current);
|
||||||
bar.setAttribute("aria-valuemax", total);
|
bar.setAttribute("aria-valuemax", total);
|
||||||
|
|
||||||
const packages = (running ?? []).map(x => `${x.author}/${x.name}`).join(", ");
|
const packages = running.map(x => `${x.author}/${x.name}`).join(", ");
|
||||||
document.getElementById("status").textContent = `Status: in progress (${current} / ${total})\n\n${packages}`;
|
document.getElementById("status").textContent = `Status: in progress (${current} / ${total})\n\n${packages}`;
|
||||||
} else {
|
} else {
|
||||||
progress.classList.add("d-none");
|
progress.classList.add("d-none");
|
||||||
@@ -98,9 +98,6 @@ window.addEventListener("load", () => {
|
|||||||
|
|
||||||
pollTask(`/tasks/${taskId}/`, true, onProgress)
|
pollTask(`/tasks/${taskId}/`, true, onProgress)
|
||||||
.then(function() { location.reload() })
|
.then(function() { location.reload() })
|
||||||
.catch(function(e) {
|
.catch(function() { location.reload() })
|
||||||
console.error(e);
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from sqlalchemy.orm import subqueryload
|
|||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func
|
||||||
from sqlalchemy_searchable import search
|
from sqlalchemy_searchable import search
|
||||||
|
|
||||||
from .models import db, PackageType, Package, ForumTopic, License, LuantiRelease, PackageRelease, User, Tag, \
|
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag, \
|
||||||
ContentWarning, PackageState, PackageDevState
|
ContentWarning, PackageState, PackageDevState
|
||||||
from .utils import is_yes, get_int_or_abort
|
from .utils import is_yes, get_int_or_abort
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class QueryBuilder:
|
|||||||
hide_wip: bool
|
hide_wip: bool
|
||||||
hide_nonfree: bool
|
hide_nonfree: bool
|
||||||
show_added: bool
|
show_added: bool
|
||||||
version: Optional[LuantiRelease]
|
version: Optional[MinetestRelease]
|
||||||
has_lang: Optional[str]
|
has_lang: Optional[str]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -163,12 +163,12 @@ class QueryBuilder:
|
|||||||
self.author = args.get("author")
|
self.author = args.get("author")
|
||||||
|
|
||||||
protocol_version = get_int_or_abort(args.get("protocol_version"))
|
protocol_version = get_int_or_abort(args.get("protocol_version"))
|
||||||
engine_version = args.get("engine_version")
|
minetest_version = args.get("engine_version")
|
||||||
if engine_version == "":
|
if minetest_version == "":
|
||||||
engine_version = None
|
minetest_version = None
|
||||||
|
|
||||||
if protocol_version or engine_version:
|
if protocol_version or minetest_version:
|
||||||
self.version = LuantiRelease.get(engine_version, protocol_version)
|
self.version = MinetestRelease.get(minetest_version, protocol_version)
|
||||||
else:
|
else:
|
||||||
self.version = None
|
self.version = None
|
||||||
|
|
||||||
|
|||||||
@@ -283,7 +283,3 @@ blockquote {
|
|||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 1rem !important;
|
margin-bottom: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[name="first_name"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -57,19 +57,12 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
content: "";
|
content: "";
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-anchor {
|
|
||||||
transition: opacity 0.15s ease-in-out;
|
|
||||||
opacity: 0.25;
|
|
||||||
margin: 0 0 0 0.25em;
|
|
||||||
font-size: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .header-anchor {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-anchor {
|
||||||
|
float: right;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-notify {
|
.badge-notify {
|
||||||
background:yellow; /* #00bc8c;*/
|
background:yellow; /* #00bc8c;*/
|
||||||
|
|||||||
@@ -92,12 +92,3 @@
|
|||||||
max-height: 1em;
|
max-height: 1em;
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-notes-body {
|
|
||||||
max-height: 20em;
|
|
||||||
overflow: hidden auto;
|
|
||||||
|
|
||||||
> *:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ from sqlalchemy import and_
|
|||||||
from sqlalchemy.dialects.postgresql import insert
|
from sqlalchemy.dialects.postgresql import insert
|
||||||
|
|
||||||
from app.models import AuditSeverity, db, NotificationType, PackageRelease, MetaPackage, Dependency, PackageType, \
|
from app.models import AuditSeverity, db, NotificationType, PackageRelease, MetaPackage, Dependency, PackageType, \
|
||||||
LuantiRelease, Package, PackageState, PackageScreenshot, PackageUpdateTrigger, PackageUpdateConfig, \
|
MinetestRelease, Package, PackageState, PackageScreenshot, PackageUpdateTrigger, PackageUpdateConfig, \
|
||||||
PackageGameSupport, PackageTranslation, Language
|
PackageGameSupport, PackageTranslation, Language
|
||||||
from app.tasks import celery, TaskError
|
from app.tasks import celery, TaskError
|
||||||
from app.utils import random_string, post_bot_message, add_system_notification, add_system_audit_log, \
|
from app.utils import random_string, post_bot_message, add_system_notification, add_system_audit_log, \
|
||||||
get_games_from_list, add_audit_log
|
get_games_from_list, add_audit_log
|
||||||
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir, get_release_notes
|
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir, get_release_notes
|
||||||
from .luanticheck import build_tree, LuantiCheckError, ContentType, PackageTreeNode
|
from .minetestcheck import build_tree, MinetestCheckError, ContentType, PackageTreeNode
|
||||||
from .webhooktasks import post_discord_webhook
|
from .webhooktasks import post_discord_webhook
|
||||||
from app import app
|
from app import app
|
||||||
from app.logic.LogicError import LogicError
|
from app.logic.LogicError import LogicError
|
||||||
@@ -51,7 +51,7 @@ def get_meta(urlstr, author):
|
|||||||
with clone_repo(urlstr, recursive=True) as repo:
|
with clone_repo(urlstr, recursive=True) as repo:
|
||||||
try:
|
try:
|
||||||
tree = build_tree(repo.working_tree_dir, author=author, repo=urlstr)
|
tree = build_tree(repo.working_tree_dir, author=author, repo=urlstr)
|
||||||
except LuantiCheckError as err:
|
except MinetestCheckError as err:
|
||||||
raise TaskError(str(err))
|
raise TaskError(str(err))
|
||||||
|
|
||||||
result = {"name": tree.name, "type": tree.type.name}
|
result = {"name": tree.name, "type": tree.type.name}
|
||||||
@@ -71,6 +71,8 @@ def get_meta(urlstr, author):
|
|||||||
data = json.loads(f.read())
|
data = json.loads(f.read())
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
result[key] = value
|
result[key] = value
|
||||||
|
except LogicError as e:
|
||||||
|
raise TaskError(e.message)
|
||||||
except JSONDecodeError as e:
|
except JSONDecodeError as e:
|
||||||
raise TaskError("Whilst reading .cdb.json: " + str(e))
|
raise TaskError("Whilst reading .cdb.json: " + str(e))
|
||||||
except IOError:
|
except IOError:
|
||||||
@@ -113,9 +115,7 @@ def post_release_check_update(self, release: PackageRelease, path):
|
|||||||
author=release.package.author.username, name=release.package.name)
|
author=release.package.author.username, name=release.package.name)
|
||||||
|
|
||||||
if tree.name is not None and release.package.name != tree.name and tree.type == ContentType.MOD:
|
if tree.name is not None and release.package.name != tree.name and tree.type == ContentType.MOD:
|
||||||
raise LuantiCheckError(f"Package name ({release.package.name}) does not match the name of the content in "
|
raise MinetestCheckError(f"Expected {tree.relative} to have technical name {release.package.name}, instead has name {tree.name}")
|
||||||
f"the release ({tree.name}). Either change the package name on ContentDB or the "
|
|
||||||
f"name in the .conf of the content. Then make a new release")
|
|
||||||
|
|
||||||
cache = {}
|
cache = {}
|
||||||
def get_meta_packages(names):
|
def get_meta_packages(names):
|
||||||
@@ -124,9 +124,6 @@ def post_release_check_update(self, release: PackageRelease, path):
|
|||||||
provides = tree.get_mod_names()
|
provides = tree.get_mod_names()
|
||||||
|
|
||||||
package = release.package
|
package = release.package
|
||||||
if not package.approved:
|
|
||||||
tree.check_for_legacy_files()
|
|
||||||
|
|
||||||
old_provided_names = set([x.name for x in package.provides])
|
old_provided_names = set([x.name for x in package.provides])
|
||||||
package.provides.clear()
|
package.provides.clear()
|
||||||
|
|
||||||
@@ -167,10 +164,10 @@ def post_release_check_update(self, release: PackageRelease, path):
|
|||||||
# Raise error on unresolved game dependencies
|
# Raise error on unresolved game dependencies
|
||||||
if package.type == PackageType.GAME and len(depends) > 0:
|
if package.type == PackageType.GAME and len(depends) > 0:
|
||||||
deps = ", ".join(depends)
|
deps = ", ".join(depends)
|
||||||
raise LuantiCheckError("Game has unresolved hard dependencies: " + deps)
|
raise MinetestCheckError("Game has unresolved hard dependencies: " + deps)
|
||||||
|
|
||||||
if package.state != PackageState.APPROVED and tree.find_license_file() is None:
|
if package.state != PackageState.APPROVED and tree.find_license_file() is None:
|
||||||
raise LuantiCheckError(
|
raise MinetestCheckError(
|
||||||
"You need to add a LICENSE.txt/.md or COPYING file to your package. See the 'Copyright Guide' for more info")
|
"You need to add a LICENSE.txt/.md or COPYING file to your package. See the 'Copyright Guide' for more info")
|
||||||
|
|
||||||
# Add dependencies
|
# Add dependencies
|
||||||
@@ -185,17 +182,17 @@ def post_release_check_update(self, release: PackageRelease, path):
|
|||||||
|
|
||||||
# Update min/max
|
# Update min/max
|
||||||
if tree.meta.get("min_minetest_version"):
|
if tree.meta.get("min_minetest_version"):
|
||||||
release.min_rel = LuantiRelease.get(tree.meta["min_minetest_version"], None)
|
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
|
||||||
|
|
||||||
if tree.meta.get("max_minetest_version"):
|
if tree.meta.get("max_minetest_version"):
|
||||||
release.max_rel = LuantiRelease.get(tree.meta["max_minetest_version"], None)
|
release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(tree.baseDir, ".cdb.json"), "r") as f:
|
with open(os.path.join(tree.baseDir, ".cdb.json"), "r") as f:
|
||||||
data = json.loads(f.read())
|
data = json.loads(f.read())
|
||||||
do_edit_package(package.author, package, False, False, data, "Post release hook")
|
do_edit_package(package.author, package, False, False, data, "Post release hook")
|
||||||
except LogicError as e:
|
except LogicError as e:
|
||||||
raise TaskError("Whilst applying .cdb.json: " + e.message)
|
raise TaskError(e.message)
|
||||||
except JSONDecodeError as e:
|
except JSONDecodeError as e:
|
||||||
raise TaskError("Whilst reading .cdb.json: " + str(e))
|
raise TaskError("Whilst reading .cdb.json: " + str(e))
|
||||||
except IOError:
|
except IOError:
|
||||||
@@ -234,7 +231,7 @@ def post_release_check_update(self, release: PackageRelease, path):
|
|||||||
|
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
except (LuantiCheckError, TaskError, LogicError) as err:
|
except (MinetestCheckError, TaskError, LogicError) as err:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
|
||||||
error_message = err.value if hasattr(err, "value") else str(err)
|
error_message = err.value if hasattr(err, "value") else str(err)
|
||||||
@@ -337,7 +334,6 @@ def check_zip_release(self, id, path):
|
|||||||
post_release_check_update(self, release, temp)
|
post_release_check_update(self, release, temp)
|
||||||
|
|
||||||
release.task_id = None
|
release.task_id = None
|
||||||
release.calculate_file_size_bytes()
|
|
||||||
release.approve(release.package.author)
|
release.approve(release.package.author)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -379,7 +375,7 @@ def import_languages(self, id, path):
|
|||||||
strict=False)
|
strict=False)
|
||||||
update_translations(release.package, tree)
|
update_translations(release.package, tree)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except (LuantiCheckError, TaskError, LogicError) as err:
|
except (MinetestCheckError, TaskError, LogicError) as err:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
|
||||||
task_url = url_for('tasks.check', id=self.request.id)
|
task_url = url_for('tasks.check', id=self.request.id)
|
||||||
@@ -417,7 +413,6 @@ def make_vcs_release(self, id, branch):
|
|||||||
|
|
||||||
release.url = "/uploads/" + filename
|
release.url = "/uploads/" + filename
|
||||||
release.task_id = None
|
release.task_id = None
|
||||||
release.calculate_file_size_bytes()
|
|
||||||
release.approve(release.package.author)
|
release.approve(release.package.author)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -480,11 +475,13 @@ def check_update_config_impl(package):
|
|||||||
if config.last_commit == commit:
|
if config.last_commit == commit:
|
||||||
if tag and config.last_tag != tag:
|
if tag and config.last_tag != tag:
|
||||||
config.last_tag = tag
|
config.last_tag = tag
|
||||||
|
db.session.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not config.last_commit:
|
if not config.last_commit:
|
||||||
config.last_commit = commit
|
config.last_commit = commit
|
||||||
config.last_tag = tag
|
config.last_tag = tag
|
||||||
|
db.session.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
if package.releases.filter_by(commit_hash=commit).count() > 0:
|
if package.releases.filter_by(commit_hash=commit).count() > 0:
|
||||||
@@ -503,6 +500,8 @@ def check_update_config_impl(package):
|
|||||||
msg = "Created release {} (Git Update Detection)".format(rel.title)
|
msg = "Created release {} (Git Update Detection)".format(rel.title)
|
||||||
add_system_audit_log(AuditSeverity.NORMAL, msg, package.get_url("packages.view"), package)
|
add_system_audit_log(AuditSeverity.NORMAL, msg, package.get_url("packages.view"), package)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
make_vcs_release.apply_async((rel.id, commit), task_id=rel.task_id)
|
make_vcs_release.apply_async((rel.id, commit), task_id=rel.task_id)
|
||||||
|
|
||||||
elif config.outdated_at is None:
|
elif config.outdated_at is None:
|
||||||
@@ -529,9 +528,10 @@ def check_update_config_impl(package):
|
|||||||
|
|
||||||
config.last_commit = commit
|
config.last_commit = commit
|
||||||
config.last_tag = tag
|
config.last_tag = tag
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@celery.task(bind=True, rate_limit="60/m")
|
@celery.task(bind=True)
|
||||||
def check_update_config(self, package_id):
|
def check_update_config(self, package_id):
|
||||||
package: Package = Package.query.get(package_id)
|
package: Package = Package.query.get(package_id)
|
||||||
if package is None:
|
if package is None:
|
||||||
@@ -542,9 +542,6 @@ def check_update_config(self, package_id):
|
|||||||
err = None
|
err = None
|
||||||
try:
|
try:
|
||||||
check_update_config_impl(package)
|
check_update_config_impl(package)
|
||||||
|
|
||||||
package.update_config.last_checked_at = datetime.datetime.now()
|
|
||||||
db.session.commit()
|
|
||||||
except GitCommandError as e:
|
except GitCommandError as e:
|
||||||
# This is needed to stop the backtrace being weird
|
# This is needed to stop the backtrace being weird
|
||||||
err = e.stderr
|
err = e.stderr
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class LuantiCheckError(Exception):
|
class MinetestCheckError(Exception):
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
@@ -43,14 +43,14 @@ class ContentType(Enum):
|
|||||||
|
|
||||||
if self == ContentType.MOD:
|
if self == ContentType.MOD:
|
||||||
if not other.is_mod_like():
|
if not other.is_mod_like():
|
||||||
raise LuantiCheckError("Expected a mod or modpack, found " + other.value)
|
raise MinetestCheckError("Expected a mod or modpack, found " + other.value)
|
||||||
|
|
||||||
elif self == ContentType.TXP:
|
elif self == ContentType.TXP:
|
||||||
if other != ContentType.UNKNOWN and other != ContentType.TXP:
|
if other != ContentType.UNKNOWN and other != ContentType.TXP:
|
||||||
raise LuantiCheckError("expected a " + self.value + ", found a " + other.value)
|
raise MinetestCheckError("expected a " + self.value + ", found a " + other.value)
|
||||||
|
|
||||||
elif other != self:
|
elif other != self:
|
||||||
raise LuantiCheckError("Expected a " + self.value + ", found a " + other.value)
|
raise MinetestCheckError("Expected a " + self.value + ", found a " + other.value)
|
||||||
|
|
||||||
|
|
||||||
from .tree import PackageTreeNode, get_base_dir
|
from .tree import PackageTreeNode, get_base_dir
|
||||||
@@ -20,7 +20,7 @@ import re
|
|||||||
import glob
|
import glob
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from . import LuantiCheckError, ContentType
|
from . import MinetestCheckError, ContentType
|
||||||
from .config import parse_conf
|
from .config import parse_conf
|
||||||
from .translation import Translation, parse_tr
|
from .translation import Translation, parse_tr
|
||||||
|
|
||||||
@@ -73,10 +73,10 @@ def check_name_list(key: str, value: list[str], relative: str, allow_star: bool
|
|||||||
if dep == "*" and allow_star:
|
if dep == "*" and allow_star:
|
||||||
continue
|
continue
|
||||||
elif " " in dep:
|
elif " " in dep:
|
||||||
raise LuantiCheckError(
|
raise MinetestCheckError(
|
||||||
f"Invalid {key} name '{dep}' at {relative}, did you forget a comma?")
|
f"Invalid {key} name '{dep}' at {relative}, did you forget a comma?")
|
||||||
else:
|
else:
|
||||||
raise LuantiCheckError(
|
raise MinetestCheckError(
|
||||||
f"Invalid {key} name '{dep}' at {relative}, names must only contain a-z0-9_.")
|
f"Invalid {key} name '{dep}' at {relative}, names must only contain a-z0-9_.")
|
||||||
|
|
||||||
|
|
||||||
@@ -90,8 +90,6 @@ class PackageTreeNode:
|
|||||||
children: list
|
children: list
|
||||||
type: ContentType
|
type: ContentType
|
||||||
strict: bool
|
strict: bool
|
||||||
has_legacy_depends: bool
|
|
||||||
has_legacy_description: bool
|
|
||||||
|
|
||||||
def __init__(self, base_dir: str, relative: str,
|
def __init__(self, base_dir: str, relative: str,
|
||||||
author: Optional[str] = None,
|
author: Optional[str] = None,
|
||||||
@@ -105,8 +103,6 @@ class PackageTreeNode:
|
|||||||
self.meta = {}
|
self.meta = {}
|
||||||
self.children = []
|
self.children = []
|
||||||
self.strict = strict
|
self.strict = strict
|
||||||
self.has_legacy_depends = False
|
|
||||||
self.has_legacy_description = False
|
|
||||||
|
|
||||||
# Detect type
|
# Detect type
|
||||||
self.type = detect_type(base_dir)
|
self.type = detect_type(base_dir)
|
||||||
@@ -114,14 +110,14 @@ class PackageTreeNode:
|
|||||||
|
|
||||||
if self.type == ContentType.GAME:
|
if self.type == ContentType.GAME:
|
||||||
if not os.path.isdir(os.path.join(base_dir, "mods")):
|
if not os.path.isdir(os.path.join(base_dir, "mods")):
|
||||||
raise LuantiCheckError("Game at {} does not have a mods/ folder".format(self.relative))
|
raise MinetestCheckError("Game at {} does not have a mods/ folder".format(self.relative))
|
||||||
self._add_children_from_mod_dir("mods")
|
self._add_children_from_mod_dir("mods")
|
||||||
elif self.type == ContentType.MOD:
|
elif self.type == ContentType.MOD:
|
||||||
if self.name and not basenamePattern.match(self.name):
|
if self.name and not basenamePattern.match(self.name):
|
||||||
raise LuantiCheckError(f"Invalid base name for mod {self.name} at {self.relative}, names must only contain a-z0-9_.")
|
raise MinetestCheckError(f"Invalid base name for mod {self.name} at {self.relative}, names must only contain a-z0-9_.")
|
||||||
|
|
||||||
if self.name and self.name in DISALLOWED_NAMES:
|
if self.name and self.name in DISALLOWED_NAMES:
|
||||||
raise LuantiCheckError(f"Forbidden mod name '{self.name}' used at {self.relative}")
|
raise MinetestCheckError(f"Forbidden mod name '{self.name}' used at {self.relative}")
|
||||||
|
|
||||||
self._check_dir_casing(["textures", "media", "sounds", "models", "locale"])
|
self._check_dir_casing(["textures", "media", "sounds", "models", "locale"])
|
||||||
elif self.type == ContentType.MODPACK:
|
elif self.type == ContentType.MODPACK:
|
||||||
@@ -139,7 +135,7 @@ class PackageTreeNode:
|
|||||||
for dir in next(os.walk(self.baseDir))[1]:
|
for dir in next(os.walk(self.baseDir))[1]:
|
||||||
lowercase = dir.lower()
|
lowercase = dir.lower()
|
||||||
if lowercase != dir and lowercase in dirs:
|
if lowercase != dir and lowercase in dirs:
|
||||||
raise LuantiCheckError(f"Incorrect case, {dir} should be {lowercase} at {self.relative}{dir}")
|
raise MinetestCheckError(f"Incorrect case, {dir} should be {lowercase} at {self.relative}{dir}")
|
||||||
|
|
||||||
def get_readme_path(self):
|
def get_readme_path(self):
|
||||||
for filename in os.listdir(self.baseDir):
|
for filename in os.listdir(self.baseDir):
|
||||||
@@ -173,12 +169,12 @@ class PackageTreeNode:
|
|||||||
for key, value in conf.items():
|
for key, value in conf.items():
|
||||||
result[key] = value
|
result[key] = value
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
raise LuantiCheckError("Error while reading {}: {}".format(meta_file_rel , e.msg))
|
raise MinetestCheckError("Error while reading {}: {}".format(meta_file_rel , e.msg))
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.strict and "release" in result:
|
if self.strict and "release" in result:
|
||||||
raise LuantiCheckError("{} should not contain 'release' key, as this is for use by ContentDB only.".format(meta_file_rel))
|
raise MinetestCheckError("{} should not contain 'release' key, as this is for use by ContentDB only.".format(meta_file_rel))
|
||||||
|
|
||||||
# description.txt
|
# description.txt
|
||||||
if "description" not in result:
|
if "description" not in result:
|
||||||
@@ -188,11 +184,6 @@ class PackageTreeNode:
|
|||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if os.path.isfile(self.baseDir + "/depends.txt"):
|
|
||||||
self.has_legacy_depends = True
|
|
||||||
if os.path.isfile(self.baseDir + "/description.txt"):
|
|
||||||
self.has_legacy_description = True
|
|
||||||
|
|
||||||
# Read dependencies
|
# Read dependencies
|
||||||
if "depends" in result or "optional_depends" in result:
|
if "depends" in result or "optional_depends" in result:
|
||||||
result["depends"] = get_csv_line(result.get("depends"))
|
result["depends"] = get_csv_line(result.get("depends"))
|
||||||
@@ -269,11 +260,11 @@ class PackageTreeNode:
|
|||||||
if not entry.startswith('.') and os.path.isdir(path):
|
if not entry.startswith('.') and os.path.isdir(path):
|
||||||
child = PackageTreeNode(path, relative + entry + "/", name=entry, strict=self.strict)
|
child = PackageTreeNode(path, relative + entry + "/", name=entry, strict=self.strict)
|
||||||
if not child.type.is_mod_like():
|
if not child.type.is_mod_like():
|
||||||
raise LuantiCheckError("Expecting mod or modpack, found {} at {} inside {}" \
|
raise MinetestCheckError("Expecting mod or modpack, found {} at {} inside {}" \
|
||||||
.format(child.type.value, child.relative, self.type.value))
|
.format(child.type.value, child.relative, self.type.value))
|
||||||
|
|
||||||
if child.name is None:
|
if child.name is None:
|
||||||
raise LuantiCheckError("Missing base name for mod at {}".format(self.relative))
|
raise MinetestCheckError("Missing base name for mod at {}".format(self.relative))
|
||||||
|
|
||||||
self.children.append(child)
|
self.children.append(child)
|
||||||
|
|
||||||
@@ -313,16 +304,6 @@ class PackageTreeNode:
|
|||||||
def get(self, key: str, default=None):
|
def get(self, key: str, default=None):
|
||||||
return self.meta.get(key, default)
|
return self.meta.get(key, default)
|
||||||
|
|
||||||
def check_for_legacy_files(self):
|
|
||||||
if self.has_legacy_depends:
|
|
||||||
raise LuantiCheckError("Found depends.txt at {}. Delete this file and use depends in mod.conf instead" \
|
|
||||||
.format(self.relative))
|
|
||||||
if self.has_legacy_description:
|
|
||||||
raise LuantiCheckError("Found description.txt at {}. Delete this file and use description in {} instead" \
|
|
||||||
.format(self.relative, self.get_meta_file_name()))
|
|
||||||
for child in self.children:
|
|
||||||
child.check_for_legacy_files()
|
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
for child in self.children:
|
for child in self.children:
|
||||||
child.validate()
|
child.validate()
|
||||||
@@ -348,6 +329,6 @@ class PackageTreeNode:
|
|||||||
ret.append(parse_tr(name))
|
ret.append(parse_tr(name))
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
relative_path = os.path.join(self.relative, os.path.relpath(name, self.baseDir))
|
relative_path = os.path.join(self.relative, os.path.relpath(name, self.baseDir))
|
||||||
raise LuantiCheckError(f"Syntax error whilst reading {relative_path}: {e}")
|
raise MinetestCheckError(f"Syntax error whilst reading {relative_path}: {e}")
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@@ -19,7 +19,7 @@ import random
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -28,7 +28,7 @@ from app import app
|
|||||||
from sqlalchemy import or_, and_
|
from sqlalchemy import or_, and_
|
||||||
|
|
||||||
from app.markdown import get_links, render_markdown
|
from app.markdown import get_links, render_markdown
|
||||||
from app.models import db, Package, PackageState, PackageRelease, PackageScreenshot, AuditLogEntry, AuditSeverity
|
from app.models import Package, db, PackageState, AuditLogEntry, AuditSeverity
|
||||||
from app.tasks import celery, TaskError
|
from app.tasks import celery, TaskError
|
||||||
from app.utils import post_bot_message, post_to_approval_thread, get_system_user, add_audit_log
|
from app.utils import post_bot_message, post_to_approval_thread, get_system_user, add_audit_log
|
||||||
|
|
||||||
@@ -131,7 +131,6 @@ def _url_exists(url: str) -> str:
|
|||||||
def _check_for_dead_links(package: Package) -> dict[str, str]:
|
def _check_for_dead_links(package: Package) -> dict[str, str]:
|
||||||
ignored_urls = set(app.config.get("LINK_CHECKER_IGNORED_URLS", ""))
|
ignored_urls = set(app.config.get("LINK_CHECKER_IGNORED_URLS", ""))
|
||||||
|
|
||||||
base_url = package.get_url("packages.view", absolute=True)
|
|
||||||
links: set[Optional[str]] = {
|
links: set[Optional[str]] = {
|
||||||
package.repo,
|
package.repo,
|
||||||
package.website,
|
package.website,
|
||||||
@@ -143,7 +142,7 @@ def _check_for_dead_links(package: Package) -> dict[str, str]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if package.desc:
|
if package.desc:
|
||||||
links.update(get_links(render_markdown(package.desc)))
|
links.update(get_links(render_markdown(package.desc), package.get_url("packages.view", absolute=True)))
|
||||||
|
|
||||||
print(f"Checking {package.title} ({len(links)} links) for broken links", file=sys.stderr)
|
print(f"Checking {package.title} ({len(links)} links) for broken links", file=sys.stderr)
|
||||||
|
|
||||||
@@ -153,15 +152,14 @@ def _check_for_dead_links(package: Package) -> dict[str, str]:
|
|||||||
if link is None:
|
if link is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
abs_link = urljoin(base_url, link)
|
url = urlparse(link)
|
||||||
url = urlparse(abs_link)
|
|
||||||
if url.scheme != "http" and url.scheme != "https":
|
if url.scheme != "http" and url.scheme != "https":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if url.hostname in ignored_urls:
|
if url.hostname in ignored_urls:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
res = _url_exists(abs_link)
|
res = _url_exists(link)
|
||||||
if res != "":
|
if res != "":
|
||||||
bad_urls[link] = res
|
bad_urls[link] = res
|
||||||
|
|
||||||
@@ -211,34 +209,3 @@ def check_package_for_broken_links(package_id: int):
|
|||||||
if msg:
|
if msg:
|
||||||
post_bot_message(package, "Broken links", msg)
|
post_bot_message(package, "Broken links", msg)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@celery.task(bind=True)
|
|
||||||
def update_file_size_bytes(self):
|
|
||||||
releases = PackageRelease.query.filter_by(file_size_bytes=0).all()
|
|
||||||
screenshots = PackageScreenshot.query.filter_by(file_size_bytes=0).all()
|
|
||||||
total = len(releases) + len(screenshots)
|
|
||||||
self.update_state(state="PROGRESS", meta={
|
|
||||||
"current": 0,
|
|
||||||
"total": total,
|
|
||||||
})
|
|
||||||
|
|
||||||
for i, release in enumerate(releases):
|
|
||||||
release.calculate_file_size_bytes()
|
|
||||||
|
|
||||||
if i % 100 == 0:
|
|
||||||
self.update_state(state="PROGRESS", meta={
|
|
||||||
"current": i + 1,
|
|
||||||
"total": total,
|
|
||||||
})
|
|
||||||
|
|
||||||
for i, ss in enumerate(screenshots):
|
|
||||||
ss.calculate_file_size_bytes()
|
|
||||||
|
|
||||||
if i % 100 == 0:
|
|
||||||
self.update_state(state="PROGRESS", meta={
|
|
||||||
"current": i + len(releases) + 1,
|
|
||||||
"total": total,
|
|
||||||
})
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from sqlalchemy import or_, and_, not_, func
|
from sqlalchemy import or_, and_
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import User, db, UserRank, ThreadReply, Package, NotificationType
|
from app.models import User, db, UserRank, ThreadReply, Package, NotificationType
|
||||||
@@ -149,37 +149,3 @@ def import_github_user_ids():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
print(f"Updated {count} users", file=sys.stderr)
|
print(f"Updated {count} users", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
@celery.task()
|
|
||||||
def do_delete_likely_spammers():
|
|
||||||
query = (User.query.filter(
|
|
||||||
and_(
|
|
||||||
User.rank == UserRank.NEW_MEMBER,
|
|
||||||
or_(
|
|
||||||
func.replace(User.website_url, ".", "").regexp_match(
|
|
||||||
func.concat("https?://[^/]*", User.username, ".*")),
|
|
||||||
),
|
|
||||||
or_(
|
|
||||||
User.website_url.ilike("%bet%"),
|
|
||||||
User.website_url.ilike("%win%"),
|
|
||||||
User.website_url.ilike("%88%"),
|
|
||||||
User.website_url.ilike("%luck%"),
|
|
||||||
User.website_url.ilike("%sport%"),
|
|
||||||
User.website_url.ilike("%lottery%"),
|
|
||||||
User.website_url.ilike("%casino%"),
|
|
||||||
User.website_url.ilike("%vip%"),
|
|
||||||
User.website_url.ilike("%assignment%"),
|
|
||||||
),
|
|
||||||
~User.packages.any(),
|
|
||||||
~User.replies.any(),
|
|
||||||
~User.reports.any(),
|
|
||||||
not_(or_(
|
|
||||||
User.website_url.ilike("%.github.io%"),
|
|
||||||
User.website_url.ilike("%.neocities.org%"),
|
|
||||||
)),
|
|
||||||
)))
|
|
||||||
|
|
||||||
for user in query.all():
|
|
||||||
db.session.delete(user)
|
|
||||||
db.session.commit()
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ def search_in_releases(self, query: str, file_filter: str, types: List[str]):
|
|||||||
handle = Popen(["zipgrep", query, release.file_path, file_filter], stdout=PIPE, encoding="UTF-8")
|
handle = Popen(["zipgrep", query, release.file_path, file_filter], stdout=PIPE, encoding="UTF-8")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
handle.wait(timeout=45)
|
handle.wait(timeout=15)
|
||||||
except TimeoutExpired:
|
except TimeoutExpired:
|
||||||
print(f"[Zipgrep] Timeout for {package.name}", file=sys.stderr)
|
print(f"[Zipgrep] Timeout for {package.name}", file=sys.stderr)
|
||||||
handle.kill()
|
handle.kill()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from . import app, utils
|
|||||||
from app.markdown import get_headings
|
from app.markdown import get_headings
|
||||||
from .models import Permission, Package, PackageState, PackageRelease
|
from .models import Permission, Package, PackageState, PackageRelease
|
||||||
from .utils import abs_url_for, url_set_query, url_set_anchor, url_current
|
from .utils import abs_url_for, url_set_query, url_set_anchor, url_current
|
||||||
from .utils.luanti_hypertext import normalize_whitespace as do_normalize_whitespace
|
from .utils.minetest_hypertext import normalize_whitespace as do_normalize_whitespace
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ Audit Log
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Audit Log</h1>
|
<h1>Audit Log</h1>
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
|
||||||
<form method="GET" action="">
|
|
||||||
{{ render_field(form.username) }}
|
|
||||||
{{ render_field(form.q) }}
|
|
||||||
{{ render_field(form.url) }}
|
|
||||||
{{ render_submit_field(form.submit) }}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% from "macros/pagination.html" import render_pagination %}
|
{% from "macros/pagination.html" import render_pagination %}
|
||||||
{% from "macros/audit_log.html" import render_audit_log %}
|
{% from "macros/audit_log.html" import render_audit_log %}
|
||||||
|
|
||||||
|
|||||||
@@ -13,20 +13,14 @@
|
|||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for('users.list_all') }}">
|
<a class="list-group-item list-group-item-action" href="{{ url_for('users.list_all') }}">
|
||||||
<i class="fas fa-users me-2"></i>
|
<i class="fas fa-users me-2"></i>
|
||||||
{{ _("User list") }}
|
User list
|
||||||
</a>
|
</a>
|
||||||
{% if current_user.rank.at_least(current_user.rank.APPROVER) %}
|
{% if current_user.rank.at_least(current_user.rank.MODERATOR) %}
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.audit') }}">
|
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.audit') }}">
|
||||||
<i class="fas fa-user-clock me-2"></i>
|
<i class="fas fa-user-clock me-2"></i>
|
||||||
{{ _("Audit Log") }}
|
{{ _("Audit Log") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.rank.at_least(current_user.rank.EDITOR) %}
|
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for('report.list_all') }}">
|
|
||||||
<i class="fas fa-user-clock me-2"></i>
|
|
||||||
Reports
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Packages</h3>
|
<h3>Packages</h3>
|
||||||
|
|||||||
@@ -18,16 +18,16 @@
|
|||||||
{{ _("Package") }}
|
{{ _("Package") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2 text-center">
|
<div class="col-2 text-center">
|
||||||
Latest release (MB)
|
Latest release / MB
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2 text-center">
|
<div class="col-2 text-center">
|
||||||
Releases (MB)
|
Releases / MB
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2 text-center">
|
<div class="col-2 text-center">
|
||||||
Screenshots (MB)
|
Screenshots / MB
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2 text-center">
|
<div class="col-2 text-center">
|
||||||
Total (MB)
|
Total / MB
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css?v=4">
|
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css?v=4">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=59">
|
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=55">
|
||||||
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
||||||
|
|
||||||
{% if noindex -%}
|
{% if noindex -%}
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
<li class="list-inline-item"><a href="{{ url_for('collections.list_all') }}">{{ _("Collections") }}</a></li>
|
<li class="list-inline-item"><a href="{{ url_for('collections.list_all') }}">{{ _("Collections") }}</a></li>
|
||||||
<li class="list-inline-item"><a href="{{ url_for('donate.donate') }}">{{ _("Support Creators") }}</a></li>
|
<li class="list-inline-item"><a href="{{ url_for('donate.donate') }}">{{ _("Support Creators") }}</a></li>
|
||||||
<li class="list-inline-item"><a href="{{ url_for('translate.translate') }}">{{ _("Translate Packages") }}</a></li>
|
<li class="list-inline-item"><a href="{{ url_for('translate.translate') }}">{{ _("Translate Packages") }}</a></li>
|
||||||
<li class="list-inline-item"><a href="https://github.com/luanti-org/contentdb">{{ _("Source Code") }}</a></li>
|
<li class="list-inline-item"><a href="https://github.com/minetest/contentdb">{{ _("Source Code") }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('set_nonfree') }}" class="my-3">
|
<form method="POST" action="{{ url_for('set_nonfree') }}" class="my-3">
|
||||||
|
|||||||
@@ -176,6 +176,11 @@
|
|||||||
<input class="btn btn-primary" name="btn_submit" type="submit" value="Comment" />
|
<input class="btn btn-primary" name="btn_submit" type="submit" value="Comment" />
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if thread.private %}
|
||||||
|
<p class="text-muted card-body my-0 pt-0">
|
||||||
|
{{ _("You can add someone to a private thread by writing @username.") }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
{{ form_scripts() }}
|
{{ form_scripts() }}
|
||||||
{{ easymde_scripts() }}
|
{{ easymde_scripts() }}
|
||||||
{% if enable_wizard %}
|
{% if enable_wizard %}
|
||||||
<script src="/static/js/polltask.js?v=4"></script>
|
<script src="/static/js/polltask.js?v=3"></script>
|
||||||
<script src="/static/js/package_create.js"></script>
|
<script src="/static/js/package_create.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script src="/static/js/package_edit.js?v=4"></script>
|
<script src="/static/js/package_edit.js?v=3"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ render_field(form.short_desc, class_="pkg_meta") }}
|
{{ render_field(form.short_desc, class_="pkg_meta") }}
|
||||||
<p class="form-text text-warning d-none" id="short_desc_mods">
|
<p class="form-text text-warning d-none" id="short_desc_mods">
|
||||||
{{ _("Tip: Don't include <i>Luanti</i>, <i>Minetest</i>, <i>mod</i>, or <i>modpack</i> anywhere in the short description. It is unnecessary and wastes characters.") }}
|
{{ _("Tip: Don't include <i>Minetest</i>, <i>mod</i>, or <i>modpack</i> anywhere in the short description. It is unnecessary and wastes characters.") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{{ render_field(form.dev_state, class_="pkg_meta", hint=_("Please choose 'Work in Progress' if your package is unstable, and shouldn't be recommended to all players")) }}
|
{{ render_field(form.dev_state, class_="pkg_meta", hint=_("Please choose 'Work in Progress' if your package is unstable, and shouldn't be recommended to all players")) }}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
pattern="[A-Za-z0-9/._-]+") }}
|
pattern="[A-Za-z0-9/._-]+") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ render_field(form.file_upload, class_="mt-3", accept=".zip") }}
|
{{ render_field(form.file_upload, fieldclass="form-control-file", class_="mt-3", accept=".zip") }}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ _("Take a look at the <a href='/help/package_config/'>Package Configuration and Releases Guide</a> for
|
{{ _("Take a look at the <a href='/help/package_config/'>Package Configuration and Releases Guide</a> for
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
{{ render_field(form.title) }}
|
{{ render_field(form.title) }}
|
||||||
{{ render_field(form.file_upload, accept="image/png,image/jpeg,image/webp") }}
|
{{ render_field(form.file_upload, fieldclass="form-control-file", accept="image/png,image/jpeg,image/webp") }}
|
||||||
{{ render_submit_field(form.submit) }}
|
{{ render_submit_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% set translations = package.translations.all() %}
|
{% set translations = package.translations.all() %}
|
||||||
{% set num = translations | length + 1 %}
|
{% set num = translations | length + 1 %}
|
||||||
|
|
||||||
<a class="btn btn-secondary float-end" href="https://api.luanti.org/translations/#translating-content-meta">
|
<a class="btn btn-secondary float-end" href="https://api.minetest.net/translations/#translating-content-meta">
|
||||||
{{ _("Help") }}
|
{{ _("Help") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html">
|
<a class="btn btn-primary me-2" href="https://rubenwardy.com/minetest_modding_book/en/quality/translations.html">
|
||||||
{{ _("Translation - Luanti Modding Book") }}
|
{{ _("Translation - Luanti Modding Book") }}
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-primary" href="https://api.luanti.org/translations/#translating-content-meta">
|
<a class="btn btn-primary" href="https://api.minetest.net/translations/#translating-content-meta">
|
||||||
{{ _("Translating content meta - lua_api.md") }}
|
{{ _("Translating content meta - lua_api.md") }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
{{ render_field(form.title) }}
|
{{ render_field(form.title) }}
|
||||||
{{ render_field(form.file_upload, accept="image/png,image/jpeg,image/webp") }}
|
{{ render_field(form.file_upload, fieldclass="form-control-file", accept="image/png,image/jpeg,image/webp") }}
|
||||||
{{ render_checkbox_field
|
{{ render_checkbox_field
|
||||||
{{ render_submit_field(form.submit) }}
|
{{ render_submit_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<h2>{% if review_thread.private %}🔒{% endif %} {{ review_thread.title }}</h2>
|
<h2>{% if review_thread.private %}🔒{% endif %} {{ review_thread.title }}</h2>
|
||||||
{% if review_thread.private %}
|
{% if review_thread.private %}
|
||||||
<p><i>
|
<p><i>
|
||||||
{{ _("This thread is only visible to its creator, package maintainers, and users of Approver rank or above.") }}
|
{{ _("This thread is only visible to its creator, package maintainers, users of Approver rank or above, and @mentioned users.") }}
|
||||||
</i></p>
|
</i></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@
|
|||||||
<h3 id="release_notes" class="card-header">
|
<h3 id="release_notes" class="card-header">
|
||||||
{{ _("Release notes for %(title)s", title=release.title) }}
|
{{ _("Release notes for %(title)s", title=release.title) }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="card-body markdown release-notes-body">
|
<div class="card-body markdown">
|
||||||
{{ release.release_notes | markdown }}
|
{{ release.release_notes | markdown }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title -%}
|
|
||||||
Edit report
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field, easymde_scripts %}
|
|
||||||
{% block scriptextra %}
|
|
||||||
{{ easymde_scripts() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{{ self.title() }}</h1>
|
|
||||||
|
|
||||||
<form method="POST" action="" enctype="multipart/form-data">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
{{ render_field(form.category) }}
|
|
||||||
{{ render_field(form.url) }}
|
|
||||||
{{ render_field(form.title) }}
|
|
||||||
{{ render_field(form.message, class_="m-0", fieldclass="form-control markdown", data_enter_submit="1") }}
|
|
||||||
{{ render_submit_field(form.submit) }}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
50
app/templates/report/index.html
Normal file
50
app/templates/report/index.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title -%}
|
||||||
|
{{ _("Report") }}
|
||||||
|
{%- endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{{ _("Report") }}</h1>
|
||||||
|
|
||||||
|
{% if not form %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ _("Due to spam, we no longer accept reports from anonymous users on this form.") }}
|
||||||
|
{{ _("Please sign in or contact the admin using the link below.") }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{{ url_for('users.login') }}" class="btn btn-primary me-2">Login</a>
|
||||||
|
<a href="{{ admin_contact_url }}" class="btn btn-secondary">Contact the admin</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
|
||||||
|
|
||||||
|
<form method="POST" action="" enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% if url %}
|
||||||
|
<p>
|
||||||
|
URL: <code>{{ url }}</code>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{{ render_field(form.message, hint=_("What are you reporting? Why are you reporting it?")) }}
|
||||||
|
{{ render_submit_field(form.submit) }}
|
||||||
|
|
||||||
|
<p class="mt-5 text-muted">
|
||||||
|
{{ _("Reports will be shared with ContentDB staff.") }}
|
||||||
|
{% if is_anon %}
|
||||||
|
{{ _("Only the admin will be able to see who made the report.") }}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p class="alert alert-info">
|
||||||
|
{{ _("Found a bug? Please report on the package's issue tracker or in a thread instead.") }}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title -%}
|
|
||||||
Reports
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{{ self.title() }}</h1>
|
|
||||||
|
|
||||||
<nav class="list-group">
|
|
||||||
{% for report in reports %}
|
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for('report.view', rid=report.id) }}">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
{% if report.is_resolved %}
|
|
||||||
<span class="badge bg-secondary me-3">
|
|
||||||
Closed
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-info me-3">
|
|
||||||
Open
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{{ report.title }}
|
|
||||||
{% if report.user %}
|
|
||||||
by {{ report.user.display_name }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
{{ report.created_at | timedelta }} ago
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<span>
|
|
||||||
No reports.
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title -%}
|
|
||||||
{{ _("Report") }}
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field, easymde_scripts %}
|
|
||||||
{% block scriptextra %}
|
|
||||||
{{ easymde_scripts() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{{ self.title() }}</h1>
|
|
||||||
|
|
||||||
{% if not form %}
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{ _("Due to spam, we no longer accept reports from anonymous users on this form.") }}
|
|
||||||
{{ _("Please sign in or contact the admin using the link below.") }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="{{ url_for('users.login') }}" class="btn btn-primary me-2">Login</a>
|
|
||||||
<a href="{{ admin_contact_url }}" class="btn btn-secondary">Contact the admin</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
<p class="text-muted">
|
|
||||||
{{ _("The full report will be visible to all ContentDB staff members, including editors and moderators.") }}
|
|
||||||
{{ _("If you are reporting something by another user, we may discuss the report with them but will not disclose who reported it.") }}
|
|
||||||
{{ _("If you are reporting something by an editor or moderator, then you should email the admin or a moderator directly to hide your identity.") }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="POST" action="" enctype="multipart/form-data">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
{{ render_field(form.category) }}
|
|
||||||
{{ render_field(form.url) }}
|
|
||||||
{{ render_field(form.title) }}
|
|
||||||
{{ render_field(form.message, class_="m-0", fieldclass="form-control markdown", data_enter_submit="1") }}
|
|
||||||
{{ render_field(form.file_upload, accept="image/png,image/jpeg,image/webp", hint=_("Optional, usually not required")) }}
|
|
||||||
{{ render_submit_field(form.submit) }}
|
|
||||||
<p class="alert alert-info mt-5">
|
|
||||||
{{ _("Found a bug? Please report on the package's issue tracker or in a thread instead.") }}
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title -%}
|
|
||||||
{{ _("We have received your report") }}
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{{ self.title() }}</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{ _("We aim to resolve your report quickly.") }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{ _("If the report is about illegal or harmful content, we aim to resolve within 48 hours.") }}
|
|
||||||
{{ _("If we find the content to be infringing, we will remove it and may warn or suspend the user.") }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% if report.thread %}
|
|
||||||
<p>
|
|
||||||
{{ _("A private thread has been created for this report. You can use it to communicate with ContentDB staff and receive updates about the report.") }}
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
<p>
|
|
||||||
{{ _("Due to limited resources, we may not contact you further about the report unless we need clarification.") }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ _("For future reference, use report id: %(report_id)s.", report_id=report.id) }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{% if report.thread %}
|
|
||||||
<a class="btn bg-primary btn-large me-2" href="{{ url_for('threads.view', id=report.thread.id) }}">{{ _("View thread") }}</a>
|
|
||||||
{% endif %}
|
|
||||||
<a class="btn bg-primary btn-large" href="{{ url_for('homepage.home') }}">{{ _("Back to home") }}</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title -%}
|
|
||||||
{{ report.title }}
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field, easymde_scripts %}
|
|
||||||
{% block scriptextra %}
|
|
||||||
{{ easymde_scripts() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% set url = url_for("report.view", rid=report.id) %}
|
|
||||||
<p class="float-end">
|
|
||||||
<a class="btn bg-secondary me-2" href="{{ url_for('admin.audit', url=url) }}">View audit log</a>
|
|
||||||
<a class="btn bg-secondary" href="{{ url_for('report.edit', rid=report.id) }}">{{ _("Edit") }}</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a class="btn bg-secondary" href="{{ url_for('report.list_all') }}">Back to reports</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
{% if report.is_resolved %}
|
|
||||||
<span class="badge bg-secondary me-3">
|
|
||||||
Closed
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-info me-3">
|
|
||||||
Open
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{{ self.title() }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<article class="row">
|
|
||||||
<div class="col-md-9">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body markdown">
|
|
||||||
{{ report.message | markdown }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="col-md-3 info-sidebar">
|
|
||||||
<dl>
|
|
||||||
<dt>Category</dt>
|
|
||||||
<dd>
|
|
||||||
{{ report.category.title }}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
<dl>
|
|
||||||
<dt>URL</dt>
|
|
||||||
<dd>
|
|
||||||
<a href="{{ report.url }}">{{ report.url }}</a>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
<dl>
|
|
||||||
<dt>Created At</dt>
|
|
||||||
<dd>
|
|
||||||
{{ report.created_at | full_datetime }}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
<dl>
|
|
||||||
<dt>Reporter</dt>
|
|
||||||
<dd>
|
|
||||||
{% if report.user %}
|
|
||||||
<a href="{{ url_for('users.profile', username=report.user.username) }}">
|
|
||||||
{{ report.user.username }}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
Anonymous
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</aside>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{% if report.attachments %}
|
|
||||||
<article>
|
|
||||||
<h2>Attachments</h2>
|
|
||||||
<ul>
|
|
||||||
{% for attachment in report.attachments %}
|
|
||||||
<li><a href="{{ attachment.url }}">{{ attachment.url }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<h2>{% if report.is_resolved %}Reopen report{% else %}Close report{% endif %}</h2>
|
|
||||||
<form method="POST" action="">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
{% if report.is_resolved %}
|
|
||||||
<button type="submit" class="btn bg-primary" name="reopen" value="true">{{ _("Reopen") }}</button>
|
|
||||||
{% else %}
|
|
||||||
<button type="submit" class="btn bg-primary" name="completed" value="true">{{ _("Completed (action taken)") }}</button>
|
|
||||||
<button type="submit" class="btn bg-primary" name="removed" value="true">{{ _("Content removed") }}</button>
|
|
||||||
<button type="submit" class="btn bg-primary" name="invalid" value="true">{{ _("Invalid / close with no action") }}</button>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<h2>Thread</h2>
|
|
||||||
{% if report.thread %}
|
|
||||||
{% from "macros/threads.html" import render_thread %}
|
|
||||||
{{ render_thread(report.thread, current_user, form=False) }}
|
|
||||||
{% else %}
|
|
||||||
<p>
|
|
||||||
No thread.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
{% if "error" in info or info.status == "FAILURE" or info.status == "REVOKED" %}
|
{% if "error" in info or info.status == "FAILURE" or info.status == "REVOKED" %}
|
||||||
<pre style="white-space: pre-wrap; word-wrap: break-word;">{{ info.error }}</pre>
|
<pre style="white-space: pre-wrap; word-wrap: break-word;">{{ info.error }}</pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
<script src="/static/js/polltask.js?v=4"></script>
|
<script src="/static/js/polltask.js?v=3"></script>
|
||||||
<noscript>
|
<noscript>
|
||||||
{{ _("Reload the page to check for updates.") }}
|
{{ _("Reload the page to check for updates.") }}
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
<aside class="row">
|
<aside class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<i>
|
<i>
|
||||||
{{ _("This thread is only visible to its creator, package maintainers, and users of Approver rank or above.") }}
|
{{ _("This thread is only visible to its creator, package maintainers, users of Approver rank or above, and @mentioned users.") }}
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@@ -128,11 +128,6 @@
|
|||||||
</aside>
|
</aside>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set report = thread.report.first() %}
|
|
||||||
{% if report and report.check_perm(current_user, "SEE_REPORT") %}
|
|
||||||
<a class="btn bg-primary btn-large" href="{{ url_for('report.view', rid=report.id) }}">View report page</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if thread.review and current_user == thread.package.author %}
|
{% if thread.review and current_user == thread.package.author %}
|
||||||
{% set flag %}
|
{% set flag %}
|
||||||
<i class="fas fa-flag mx-2"></i>
|
<i class="fas fa-flag mx-2"></i>
|
||||||
|
|||||||
@@ -5,28 +5,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if reports %}
|
|
||||||
<h2 class="mb-4">{{ _("Reports") }}</h2>
|
|
||||||
<nav class="list-group">
|
|
||||||
{% for report in reports %}
|
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for('report.view', rid=report.id) }}">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<span class="badge bg-secondary me-3">{{ report.category.title }}</span>
|
|
||||||
{{ report.title }}
|
|
||||||
{% if report.user %}
|
|
||||||
by {{ report.user.display_name }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
{{ report.created_at | timedelta }} ago
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2 class="mb-4">{{ _("Approval Queue") }}</h2>
|
<h2 class="mb-4">{{ _("Approval Queue") }}</h2>
|
||||||
{% if can_approve_scn and screenshots %}
|
{% if can_approve_scn and screenshots %}
|
||||||
<div class="card my-4">
|
<div class="card my-4">
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<a class="btn btn-secondary float-end" href="https://dev.luanti.org/Translation#Translating_mods_and_games">
|
<a class="btn btn-secondary float-end" href="https://dev.minetest.net/Translation#Translating_mods_and_games">
|
||||||
{{ _("How to translate a mod / game") }}
|
{{ _("How to translate a mod / game") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
{{ _("Please raise a report to request account deletion.") }}
|
{{ _("Please raise a report to request account deletion.") }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a class="btn btn-secondary" href="{{ url_for('report.report', url=url_current(), title='Delete my account', category='account_deletion') }}">{{ _("Report") }}</a>
|
<a class="btn btn-secondary" href="{{ url_for('report.report', url=url_current(), message="Delete my account") }}">{{ _("Report") }}</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.website_url and user.check_perm(user, "LINK_TO_WEBSITE") %}
|
{% if user.website_url %}
|
||||||
<a class="btn" href="{{ user.website_url }}" rel="ugc">
|
<a class="btn" href="{{ user.website_url }}" rel="ugc">
|
||||||
<i class="fas fa-globe-europe"></i>
|
<i class="fas fa-globe-europe"></i>
|
||||||
<span class="count">
|
<span class="count">
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.donate_url and user.check_perm(user, "LINK_TO_WEBSITE") %}
|
{% if user.donate_url %}
|
||||||
<a class="btn" href="{{ user.donate_url }}" rel="ugc">
|
<a class="btn" href="{{ user.donate_url }}" rel="ugc">
|
||||||
<i class="fas fa-donate"></i>
|
<i class="fas fa-donate"></i>
|
||||||
<span class="count">
|
<span class="count">
|
||||||
|
|||||||
@@ -67,13 +67,6 @@
|
|||||||
{{ render_field(form.donate_url, tabindex=233) }}
|
{{ render_field(form.donate_url, tabindex=233) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not user.check_perm(user, "LINK_TO_WEBSITE") %}
|
|
||||||
<p>
|
|
||||||
{{ _("Website URLs will not be shown whilst you are a 'New Member'.") }}
|
|
||||||
{{ _("To become a full member, create a review, comment, or package and wait 7 days.") }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ render_submit_field(form.submit, tabindex=280) }}
|
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -34,8 +34,6 @@
|
|||||||
</p>
|
</p>
|
||||||
{{ render_field(form.question, hint=_("Please prove that you are human")) }}
|
{{ render_field(form.question, hint=_("Please prove that you are human")) }}
|
||||||
|
|
||||||
<input class="form-control" id="first_name" name="first_name" type="text" value="">
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ _("By signing up, you agree to the <a href='/terms/' target='_blank'>Terms of Service</a> and <a href='/privacy_policy/' target='_blank'>Privacy Policy</a>.") }}
|
{{ _("By signing up, you agree to the <a href='/terms/' target='_blank'>Terms of Service</a> and <a href='/privacy_policy/' target='_blank'>Privacy Policy</a>.") }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import List, Tuple, Optional
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
from app.default_data import populate_test_data
|
from app.default_data import populate_test_data
|
||||||
from app.models import db, License, PackageType, User, Package, PackageState, PackageRelease, LuantiRelease
|
from app.models import db, License, PackageType, User, Package, PackageState, PackageRelease, MinetestRelease
|
||||||
from .utils import parse_json, validate_package_list
|
from .utils import parse_json, validate_package_list
|
||||||
from .utils import client # noqa
|
from .utils import client # noqa
|
||||||
|
|
||||||
@@ -32,10 +32,10 @@ def make_package(name: str, versions: List[Tuple[Optional[str], Optional[str]]])
|
|||||||
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
|
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
|
||||||
|
|
||||||
if minv:
|
if minv:
|
||||||
rel.min_rel = LuantiRelease.query.filter_by(name=minv).first()
|
rel.min_rel = MinetestRelease.query.filter_by(name=minv).first()
|
||||||
assert rel.min_rel
|
assert rel.min_rel
|
||||||
if maxv:
|
if maxv:
|
||||||
rel.max_rel = LuantiRelease.query.filter_by(name=maxv).first()
|
rel.max_rel = MinetestRelease.query.filter_by(name=maxv).first()
|
||||||
assert rel.max_rel
|
assert rel.max_rel
|
||||||
|
|
||||||
rel.approved = True
|
rel.approved = True
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class MockEntry:
|
|||||||
self.causer = causer
|
self.causer = causer
|
||||||
self.created_at = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z")
|
self.created_at = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z")
|
||||||
self.title = title
|
self.title = title
|
||||||
self.description = None
|
|
||||||
self.package = MockPackage(package_id)
|
self.package = MockPackage(package_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,10 @@ def test_cycle():
|
|||||||
support.on_update(modA)
|
support.on_update(modA)
|
||||||
|
|
||||||
assert support.all_errors == {
|
assert support.all_errors == {
|
||||||
|
"author/mod_b: Dependency cycle detected: author/mod_a -> author/mod_b -> author/mod_a",
|
||||||
|
"author/mod_a: Dependency cycle detected: author/mod_a -> author/mod_b -> author/mod_a",
|
||||||
|
"author/mod_b: Dependency cycle detected: author/mod_b -> author/mod_a -> author/mod_b",
|
||||||
|
"author/mod_a: Dependency cycle detected: author/mod_b -> author/mod_a -> author/mod_b",
|
||||||
"author/mod_b: Unable to fulfill dependency mod_a",
|
"author/mod_b: Unable to fulfill dependency mod_a",
|
||||||
"author/mod_a: Unable to fulfill dependency mod_b"
|
"author/mod_a: Unable to fulfill dependency mod_b"
|
||||||
}
|
}
|
||||||
@@ -232,6 +236,8 @@ def test_cycle_fails_safely():
|
|||||||
"author/mod_b: Unable to fulfill dependency mod_c",
|
"author/mod_b: Unable to fulfill dependency mod_c",
|
||||||
"author/mod_d: Unable to fulfill dependency mod_b",
|
"author/mod_d: Unable to fulfill dependency mod_b",
|
||||||
"author/mod_c: Unable to fulfill dependency mod_b",
|
"author/mod_c: Unable to fulfill dependency mod_b",
|
||||||
|
"author/mod_c: Dependency cycle detected: author/mod_b -> author/mod_c -> author/mod_b",
|
||||||
|
"author/mod_b: Dependency cycle detected: author/mod_b -> author/mod_c -> author/mod_b"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -376,6 +382,10 @@ def test_update_cycle():
|
|||||||
support.on_update(game1)
|
support.on_update(game1)
|
||||||
|
|
||||||
assert support.all_errors == {
|
assert support.all_errors == {
|
||||||
|
"author/mod_c: Dependency cycle detected: author/mod_a -> author/mod_c -> author/mod_a",
|
||||||
|
"author/mod_a: Dependency cycle detected: author/mod_a -> author/mod_c -> author/mod_a",
|
||||||
|
"author/mod_c: Dependency cycle detected: author/mod_c -> author/mod_a -> author/mod_c",
|
||||||
|
"author/mod_a: Dependency cycle detected: author/mod_c -> author/mod_a -> author/mod_c",
|
||||||
"author/mod_a: Unable to fulfill dependency mod_c",
|
"author/mod_a: Unable to fulfill dependency mod_c",
|
||||||
"author/mod_c: Unable to fulfill dependency mod_a"
|
"author/mod_c: Unable to fulfill dependency mod_a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ def test_missing_hard_deps(get_forum_topic):
|
|||||||
mock_package = MockPackageHelper(PackageType.MOD)
|
mock_package = MockPackageHelper(PackageType.MOD)
|
||||||
mock_package.add_release()
|
mock_package.add_release()
|
||||||
mock_package.add_missing_hard_deps()
|
mock_package.add_missing_hard_deps()
|
||||||
mock_package.add_screenshot()
|
|
||||||
|
|
||||||
topic = MagicMock()
|
topic = MagicMock()
|
||||||
topic.author = mock_package.package.author
|
topic.author = mock_package.package.author
|
||||||
@@ -104,7 +103,6 @@ def test_requires_multiple_issues():
|
|||||||
mock_package.add_release()
|
mock_package.add_release()
|
||||||
mock_package.set_license("Other", "Other")
|
mock_package.set_license("Other", "Other")
|
||||||
mock_package.set_no_game_support()
|
mock_package.set_no_game_support()
|
||||||
mock_package.add_screenshot()
|
|
||||||
|
|
||||||
notes = package_approval.validate_package_for_approval(mock_package.package)
|
notes = package_approval.validate_package_for_approval(mock_package.package)
|
||||||
assert len(notes) == 5
|
assert len(notes) == 5
|
||||||
@@ -122,7 +120,6 @@ def test_requires_multiple_issues():
|
|||||||
def test_forum_topic_author_mismatch(get_forum_topic):
|
def test_forum_topic_author_mismatch(get_forum_topic):
|
||||||
mock_package = MockPackageHelper()
|
mock_package = MockPackageHelper()
|
||||||
mock_package.add_release()
|
mock_package.add_release()
|
||||||
mock_package.add_screenshot()
|
|
||||||
|
|
||||||
topic = MagicMock()
|
topic = MagicMock()
|
||||||
get_forum_topic.return_value = topic
|
get_forum_topic.return_value = topic
|
||||||
@@ -139,7 +136,6 @@ def test_forum_topic_author_mismatch(get_forum_topic):
|
|||||||
def test_passes(get_forum_topic):
|
def test_passes(get_forum_topic):
|
||||||
mock_package = MockPackageHelper()
|
mock_package = MockPackageHelper()
|
||||||
mock_package.add_release()
|
mock_package.add_release()
|
||||||
mock_package.add_screenshot()
|
|
||||||
|
|
||||||
topic = MagicMock()
|
topic = MagicMock()
|
||||||
topic.author = mock_package.package.author
|
topic.author = mock_package.package.author
|
||||||
@@ -172,8 +168,8 @@ def test_games_txp_must_have_unique_name(get_forum_topic):
|
|||||||
@patch("app.logic.package_approval.get_conflicting_mod_names", MagicMock(return_value=set()))
|
@patch("app.logic.package_approval.get_conflicting_mod_names", MagicMock(return_value=set()))
|
||||||
@patch("app.logic.package_approval.count_packages_with_forum_topic", MagicMock(return_value=1))
|
@patch("app.logic.package_approval.count_packages_with_forum_topic", MagicMock(return_value=1))
|
||||||
@patch("app.logic.package_approval.get_forum_topic")
|
@patch("app.logic.package_approval.get_forum_topic")
|
||||||
def test_require_screenshots(get_forum_topic):
|
def test_games_txp_require_screenshots(get_forum_topic):
|
||||||
mock_package = MockPackageHelper(PackageType.MOD)
|
mock_package = MockPackageHelper(PackageType.GAME)
|
||||||
mock_package.add_release()
|
mock_package.add_release()
|
||||||
|
|
||||||
topic = MagicMock()
|
topic = MagicMock()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.tasks.luanticheck.translation import parse_tr
|
from app.tasks.minetestcheck.translation import parse_tr
|
||||||
|
|
||||||
|
|
||||||
def test_parses_tr():
|
def test_parses_tr():
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from app.utils.luanti_hypertext import html_to_luanti
|
from app.utils.minetest_hypertext import html_to_minetest
|
||||||
|
|
||||||
|
|
||||||
conquer_html = """
|
conquer_html = """
|
||||||
@@ -74,7 +74,7 @@ page_url = "https://example.com/a/b/"
|
|||||||
|
|
||||||
|
|
||||||
def test_conquer():
|
def test_conquer():
|
||||||
assert html_to_luanti(conquer_html, page_url)["body"].strip() == conquer_expected.strip()
|
assert html_to_minetest(conquer_html, page_url)["body"].strip() == conquer_expected.strip()
|
||||||
|
|
||||||
|
|
||||||
def test_images():
|
def test_images():
|
||||||
@@ -83,7 +83,7 @@ def test_images():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
expected = "<img name=image_0 width=128 height=128>"
|
expected = "<img name=image_0 width=128 height=128>"
|
||||||
result = html_to_luanti(html, page_url)
|
result = html_to_minetest(html, page_url)
|
||||||
assert result["body"].strip() == expected.strip()
|
assert result["body"].strip() == expected.strip()
|
||||||
assert len(result["images"]) == 1
|
assert len(result["images"]) == 1
|
||||||
assert result["images"]["image_0"] == "https://example.com/path/to/img.png"
|
assert result["images"]["image_0"] == "https://example.com/path/to/img.png"
|
||||||
@@ -95,7 +95,7 @@ def test_images_removed():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
expected = "<action name=image_0><u>Image: alt</u></action>"
|
expected = "<action name=image_0><u>Image: alt</u></action>"
|
||||||
result = html_to_luanti(html, page_url, 7, False)
|
result = html_to_minetest(html, page_url, 7, False)
|
||||||
assert result["body"].strip() == expected.strip()
|
assert result["body"].strip() == expected.strip()
|
||||||
assert len(result["images"]) == 0
|
assert len(result["images"]) == 0
|
||||||
assert result["links"]["image_0"] == "https://example.com/path/to/img.png"
|
assert result["links"]["image_0"] == "https://example.com/path/to/img.png"
|
||||||
@@ -112,7 +112,7 @@ def test_links_relative_absolute():
|
|||||||
"<action name=link_1><u>Absolute</u></action> " \
|
"<action name=link_1><u>Absolute</u></action> " \
|
||||||
"<action name=link_2><u>Other domain</u></action>"
|
"<action name=link_2><u>Other domain</u></action>"
|
||||||
|
|
||||||
result = html_to_luanti(html, page_url, 7, False)
|
result = html_to_minetest(html, page_url, 7, False)
|
||||||
assert result["body"].strip() == expected.strip()
|
assert result["body"].strip() == expected.strip()
|
||||||
assert result["links"]["link_0"] == "https://example.com/a/b/relative"
|
assert result["links"]["link_0"] == "https://example.com/a/b/relative"
|
||||||
assert result["links"]["link_1"] == "https://example.com/absolute"
|
assert result["links"]["link_1"] == "https://example.com/absolute"
|
||||||
@@ -134,7 +134,7 @@ def test_bullets():
|
|||||||
"<img name=blank.png width=32 height=1>• sub two\n\n" \
|
"<img name=blank.png width=32 height=1>• sub two\n\n" \
|
||||||
"<img name=blank.png width=16 height=1>• four\n"
|
"<img name=blank.png width=16 height=1>• four\n"
|
||||||
|
|
||||||
result = html_to_luanti(html, page_url)
|
result = html_to_minetest(html, page_url)
|
||||||
assert result["body"].strip() == expected.strip()
|
assert result["body"].strip() == expected.strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ def test_table():
|
|||||||
expected = "<action name=link_0><u>(view table in browser)</u></action>\n\n" \
|
expected = "<action name=link_0><u>(view table in browser)</u></action>\n\n" \
|
||||||
"<b>Heading</b>\n" \
|
"<b>Heading</b>\n" \
|
||||||
"<action name=link_1><u>(view table in browser)</u></action>"
|
"<action name=link_1><u>(view table in browser)</u></action>"
|
||||||
result = html_to_luanti(html, page_url)
|
result = html_to_minetest(html, page_url)
|
||||||
assert result["body"].strip() == expected.strip()
|
assert result["body"].strip() == expected.strip()
|
||||||
assert result["links"]["link_0"] == f"{page_url}#with-id"
|
assert result["links"]["link_0"] == f"{page_url}#with-id"
|
||||||
assert result["links"]["link_1"] == f"{page_url}#heading"
|
assert result["links"]["link_1"] == f"{page_url}#heading"
|
||||||
@@ -170,7 +170,7 @@ def test_inline():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
expected = "<b>One <i>two</i> three</b>"
|
expected = "<b>One <i>two</i> three</b>"
|
||||||
result = html_to_luanti(html, page_url)
|
result = html_to_minetest(html, page_url)
|
||||||
assert result["body"].strip() == expected.strip()
|
assert result["body"].strip() == expected.strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ def test_escape():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
expected = r"<b>One <i>t\\w\<o\></i> three</b>"
|
expected = r"<b>One <i>t\\w\<o\></i> three</b>"
|
||||||
result = html_to_luanti(html, page_url)
|
result = html_to_minetest(html, page_url)
|
||||||
assert result["body"].strip() == expected.strip()
|
assert result["body"].strip() == expected.strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -190,5 +190,5 @@ def test_unknown_attr():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
expected = r"<action name=link_0><u>link</u></action>"
|
expected = r"<action name=link_0><u>link</u></action>"
|
||||||
result = html_to_luanti(html, page_url)
|
result = html_to_minetest(html, page_url)
|
||||||
assert result["body"].strip() == expected.strip()
|
assert result["body"].strip() == expected.strip()
|
||||||
@@ -32,9 +32,8 @@ def test_web_is_not_bot():
|
|||||||
"Chrome/125.0.0.0 Safari/537.36").is_bot
|
"Chrome/125.0.0.0 Safari/537.36").is_bot
|
||||||
|
|
||||||
|
|
||||||
def test_luanti_is_not_bot():
|
def test_minetest_is_not_bot():
|
||||||
assert not user_agents.parse("Minetest/5.5.1 (Linux/4.14.193+-ab49821 aarch64)").is_bot
|
assert not user_agents.parse("Minetest/5.5.1 (Linux/4.14.193+-ab49821 aarch64)").is_bot
|
||||||
assert not user_agents.parse("Luanti/5.12.0 (Linux/4.14.193+-ab49821 aarch64)").is_bot
|
|
||||||
|
|
||||||
|
|
||||||
def test_crawlers_are_bots():
|
def test_crawlers_are_bots():
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ def make_indent(w):
|
|||||||
return f"<img name=blank.png width={16*w} height=1>"
|
return f"<img name=blank.png width={16*w} height=1>"
|
||||||
|
|
||||||
|
|
||||||
class LuantiHTMLParser(HTMLParser):
|
class MinetestHTMLParser(HTMLParser):
|
||||||
def __init__(self, page_url: str, include_images: bool, link_prefix: str):
|
def __init__(self, page_url: str, include_images: bool, link_prefix: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.page_url = page_url
|
self.page_url = page_url
|
||||||
@@ -224,8 +224,8 @@ class LuantiHTMLParser(HTMLParser):
|
|||||||
self.current_line += f"&{name};"
|
self.current_line += f"&{name};"
|
||||||
|
|
||||||
|
|
||||||
def html_to_luanti(html, page_url: str, formspec_version: int = 7, include_images: bool = True, link_prefix: str = "link_"):
|
def html_to_minetest(html, page_url: str, formspec_version: int = 7, include_images: bool = True, link_prefix: str = "link_"):
|
||||||
parser = LuantiHTMLParser(page_url, include_images, link_prefix)
|
parser = MinetestHTMLParser(page_url, include_images, link_prefix)
|
||||||
parser.feed(html)
|
parser.feed(html)
|
||||||
parser.finish_line()
|
parser.finish_line()
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ def package_reviews_as_hypertext(package: Package, formspec_version: int = 7):
|
|||||||
for review in reviews:
|
for review in reviews:
|
||||||
review: PackageReview
|
review: PackageReview
|
||||||
html = render_markdown(review.thread.first_reply.comment)
|
html = render_markdown(review.thread.first_reply.comment)
|
||||||
content = html_to_luanti(html, package.get_url("packages.view", absolute=True),
|
content = html_to_minetest(html, package.get_url("packages.view", absolute=True),
|
||||||
formspec_version, False, f"review_{review.id}_")
|
formspec_version, False, f"review_{review.id}_")
|
||||||
links.update(content["links"])
|
links.update(content["links"])
|
||||||
comment_body = content["body"].rstrip()
|
comment_body = content["body"].rstrip()
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
version: '3'
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: "postgres:14"
|
image: "postgres:14"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Luanti's use of the API
|
# Minetest's use of the API
|
||||||
|
|
||||||
This document explains how Luanti's ContentDB client interacts with ContentDB.
|
This document explains how Minetest's ContentDB client interacts with ContentDB.
|
||||||
This is useful both for implementing your own client for ContentDB to install mods,
|
This is useful both for implementing your own client for ContentDB to install mods,
|
||||||
or for implementing ContentDB compatible servers.
|
or for implementing ContentDB compatible servers.
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ Example response:
|
|||||||
`type` is one of `mod`, `game`, or `txp`.
|
`type` is one of `mod`, `game`, or `txp`.
|
||||||
|
|
||||||
`release` is the release ID. Newer releases have higher IDs.
|
`release` is the release ID. Newer releases have higher IDs.
|
||||||
Luanti compares this ID to a locally stored version to detect whether a package has updates.
|
Minetest compares this ID to a locally stored version to detect whether a package has updates.
|
||||||
|
|
||||||
Because the client specifies the engine version information, the response must contain a release
|
Because the client specifies the engine version information, the response must contain a release
|
||||||
number and the package must be downloadable.
|
number and the package must be downloadable.
|
||||||
@@ -62,7 +62,7 @@ track the installed release to detect updates in the future.
|
|||||||
|
|
||||||
### Short version
|
### Short version
|
||||||
|
|
||||||
Luanti uses `/api/packages/<author>/<name>/dependencies/?only_hard=1` to find out the hard
|
Minetest uses `/api/packages/<author>/<name>/dependencies/?only_hard=1` to find out the hard
|
||||||
dependencies for a package.
|
dependencies for a package.
|
||||||
|
|
||||||
Then, it resolves each dependency recursively.
|
Then, it resolves each dependency recursively.
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 1acc6e90bbac
|
|
||||||
Revises: 57b7fbc174cf
|
|
||||||
Create Date: 2025-08-26 20:23:29.086541
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '1acc6e90bbac'
|
|
||||||
down_revision = '57b7fbc174cf'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table('package_update_config', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('last_checked_at', sa.DateTime(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table('package_update_config', schema=None) as batch_op:
|
|
||||||
batch_op.drop_column('last_checked_at')
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 1e08d7e4c15d
|
|
||||||
Revises: 9689a71efe88
|
|
||||||
Create Date: 2025-08-26 14:43:30.501823
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '1e08d7e4c15d'
|
|
||||||
down_revision = '9689a71efe88'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
status = postgresql.ENUM('ACCOUNT_DELETION', 'COPYRIGHT', 'USER_CONDUCT', 'ILLEGAL_HARMFUL', 'APPEAL', 'OTHER', name='reportcategory')
|
|
||||||
status.create(op.get_bind())
|
|
||||||
with op.batch_alter_table('report', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('category', sa.Enum('ACCOUNT_DELETION', 'COPYRIGHT', 'USER_CONDUCT', 'ILLEGAL_HARMFUL', 'APPEAL', 'OTHER', name='reportcategory'), nullable=False, server_default="OTHER"))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table('report', schema=None) as batch_op:
|
|
||||||
batch_op.drop_column('category')
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 242fd82077bb
|
|
||||||
Revises: 1acc6e90bbac
|
|
||||||
Create Date: 2025-09-01 10:00:39.263576
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '242fd82077bb'
|
|
||||||
down_revision = '1acc6e90bbac'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.create_table('report_attachment',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('report_id', sa.String(length=24), nullable=False),
|
|
||||||
sa.Column('url', sa.String(length=100), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['report_id'], ['report.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_table('report_attachment')
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '3052712496e4'
|
|
||||||
down_revision = '663521dfe86d'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('report',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('thread_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('url', sa.String(), nullable=True),
|
|
||||||
sa.Column('title', sa.Unicode(length=300), nullable=False),
|
|
||||||
sa.Column('message', sa.UnicodeText(), nullable=False),
|
|
||||||
sa.Column('is_resolved', sa.Boolean(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_table('report')
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 57b7fbc174cf
|
|
||||||
Revises: 1e08d7e4c15d
|
|
||||||
Create Date: 2025-08-26 19:23:22.446424
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy import text
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '57b7fbc174cf'
|
|
||||||
down_revision = '1e08d7e4c15d'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.execute(text("COMMIT"))
|
|
||||||
op.execute(text("ALTER TYPE reportcategory ADD VALUE 'REVIEW' AFTER 'ILLEGAL_HARMFUL'"))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
pass
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '663521dfe86d'
|
|
||||||
down_revision = 'c181c6c88bae'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.rename_table("minetest_release", "luanti_release")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.rename_table("luanti_release", "minetest_release")
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 8f55dfbec825
|
|
||||||
Revises: 242fd82077bb
|
|
||||||
Create Date: 2025-09-23 15:21:06.445012
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy import text
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '8f55dfbec825'
|
|
||||||
down_revision = '242fd82077bb'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.execute(text("COMMIT"))
|
|
||||||
op.execute(text("ALTER TYPE reportcategory ADD VALUE 'SPAM' AFTER 'USER_CONDUCT'"))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
pass
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 9689a71efe88
|
|
||||||
Revises: 3052712496e4
|
|
||||||
Create Date: 2025-08-26 14:24:02.045713
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '9689a71efe88'
|
|
||||||
down_revision = '3052712496e4'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table('report', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('id',
|
|
||||||
existing_type=sa.INTEGER(),
|
|
||||||
type_=sa.String(length=24),
|
|
||||||
existing_nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table('report', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('id',
|
|
||||||
existing_type=sa.String(length=24),
|
|
||||||
type_=sa.INTEGER(),
|
|
||||||
existing_nullable=False)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: c181c6c88bae
|
|
||||||
Revises: daa040b727b2
|
|
||||||
Create Date: 2025-07-02 17:21:33.554960
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'c181c6c88bae'
|
|
||||||
down_revision = 'daa040b727b2'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.add_column('package_release',
|
|
||||||
sa.Column('file_size_bytes', sa.Integer(), nullable=False, server_default="0"))
|
|
||||||
op.add_column('package_screenshot',
|
|
||||||
sa.Column('file_size_bytes', sa.Integer(), nullable=False, server_default="0"))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_column('package', 'file_size_bytes')
|
|
||||||
op.drop_column('package_screenshot', 'file_size_bytes')
|
|
||||||
@@ -18,7 +18,7 @@ depends_on = None
|
|||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# Source: https://github.com/luanti-org/luanti/blob/master/builtin/mainmenu/settings/dlg_settings.lua#L156
|
# Source: https://github.com/minetest/minetest/blob/master/builtin/mainmenu/settings/dlg_settings.lua#L156
|
||||||
languages = {
|
languages = {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
# "ar": "", blacklisted
|
# "ar": "", blacklisted
|
||||||
|
|||||||
@@ -1,83 +1,80 @@
|
|||||||
alembic==1.16.2
|
alembic==1.13.1
|
||||||
amqp==5.3.1
|
amqp==5.2.0
|
||||||
async-timeout==5.0.1
|
async-timeout==4.0.3
|
||||||
babel==2.17.0
|
Babel==2.15.0
|
||||||
bcrypt==4.3.0
|
bcrypt==4.1.3
|
||||||
beautifulsoup4==4.13.4
|
beautifulsoup4==4.12.3
|
||||||
billiard==4.2.1
|
billiard==4.2.0
|
||||||
bleach==6.2.0
|
bleach==6.1.0
|
||||||
blinker==1.9.0
|
blinker==1.8.2
|
||||||
celery==5.5.3
|
celery==5.4.0
|
||||||
certifi==2025.6.15
|
certifi==2024.2.2
|
||||||
charset-normalizer==3.4.2
|
charset-normalizer==3.3.2
|
||||||
click==8.2.1
|
click==8.1.7
|
||||||
click-didyoumean==0.3.1
|
click-didyoumean==0.3.1
|
||||||
click-plugins==1.1.1.2
|
click-plugins==1.1.1
|
||||||
click-repl==0.3.0
|
click-repl==0.3.0
|
||||||
coverage==7.9.1
|
coverage==7.5.1
|
||||||
deep-compare==1.0.5
|
deep-compare==1.0.5
|
||||||
dnspython==2.7.0
|
dnspython==2.6.1
|
||||||
email_validator==2.2.0
|
email_validator==2.1.1
|
||||||
exceptiongroup==1.3.0
|
exceptiongroup==1.2.1
|
||||||
Flask==3.1.1
|
Flask==3.0.3
|
||||||
flask-babel==4.0.0
|
flask-babel==4.0.0
|
||||||
Flask-FlatPages==0.8.3
|
Flask-FlatPages==0.8.2
|
||||||
Flask-Login==0.6.3
|
Flask-Login==0.6.3
|
||||||
Flask-Mail==0.10.0
|
Flask-Mail==0.10.0
|
||||||
Flask-Migrate==4.1.0
|
Flask-Migrate==4.0.7
|
||||||
Flask-SQLAlchemy==3.1.1
|
Flask-SQLAlchemy==3.1.1
|
||||||
Flask-WTF==1.2.2
|
Flask-WTF==1.2.1
|
||||||
git-archive-all==1.23.1
|
git-archive-all==1.23.1
|
||||||
gitdb==4.0.12
|
gitdb==4.0.11
|
||||||
GitHub-Flask==3.2.0
|
GitHub-Flask==3.2.0
|
||||||
GitPython==3.1.44
|
GitPython==3.1.43
|
||||||
greenlet==3.2.3
|
greenlet==3.0.3
|
||||||
idna==3.10
|
idna==3.7
|
||||||
iniconfig==2.1.0
|
iniconfig==2.0.0
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
Jinja2==3.1.6
|
Jinja2==3.1.4
|
||||||
kombu==5.5.4
|
kombu==5.3.7
|
||||||
libsass==0.23.0
|
libsass==0.23.0
|
||||||
linkify-it-py==2.0.3
|
lxml==5.2.2
|
||||||
lxml==6.0.0
|
Mako==1.3.5
|
||||||
Mako==1.3.10
|
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
MarkupSafe==3.0.2
|
|
||||||
mdit-py-plugins==0.4.2
|
mdit-py-plugins==0.4.2
|
||||||
mdurl==0.1.2
|
linkify-it-py==2.0.3
|
||||||
packaging==25.0
|
MarkupSafe==2.1.5
|
||||||
|
packaging==24.0
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
pillow==11.3.0
|
pillow==10.3.0
|
||||||
pluggy==1.6.0
|
pluggy==1.5.0
|
||||||
prompt_toolkit==3.0.51
|
prompt-toolkit==3.0.43
|
||||||
psycopg2==2.9.10
|
psycopg2==2.9.9
|
||||||
Pygments==2.19.2
|
Pygments==2.18.0
|
||||||
pytest==8.4.1
|
pytest==8.2.1
|
||||||
pytest-cov==6.2.1
|
pytest-cov==5.0.0
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
pytz==2025.2
|
pytz==2024.1
|
||||||
PyYAML==6.0.2
|
PyYAML==6.0.1
|
||||||
redis==5.3.0
|
redis==5.0.4
|
||||||
requests==2.32.4
|
requests==2.32.2
|
||||||
sentry-sdk[flask]==2.32.0
|
sentry-sdk[flask]==2.3.1
|
||||||
six==1.17.0
|
six==1.16.0
|
||||||
smmap==5.0.2
|
smmap==5.0.1
|
||||||
soupsieve==2.7
|
soupsieve==2.5
|
||||||
SQLAlchemy==2.0.41
|
SQLAlchemy==2.0.30
|
||||||
sqlalchemy-searchable==2.1.0
|
sqlalchemy-searchable==2.1.0
|
||||||
SQLAlchemy-Utils==0.41.2
|
SQLAlchemy-Utils==0.41.2
|
||||||
tomli==2.2.1
|
tomli==2.0.1
|
||||||
typing_extensions==4.14.0
|
typing_extensions==4.11.0
|
||||||
tzdata==2025.2
|
tzdata==2024.1
|
||||||
ua-parser==1.0.1
|
ua-parser==0.18.0
|
||||||
ua-parser-builtins==0.18.0.post1
|
urllib3==2.2.1
|
||||||
uc-micro-py==1.0.3
|
|
||||||
urllib3==2.5.0
|
|
||||||
user-agents==2.2.0
|
user-agents==2.2.0
|
||||||
validators==0.35.0
|
validators==0.28.1
|
||||||
vine==5.1.0
|
vine==5.1.0
|
||||||
wcwidth==0.2.13
|
wcwidth==0.2.13
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
Werkzeug==3.1.3
|
Werkzeug==3.0.3
|
||||||
WTForms==3.2.1
|
WTForms==3.1.2
|
||||||
WTForms-SQLAlchemy==0.4.2
|
WTForms-SQLAlchemy==0.4.1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user