Use snake_case for method names

This commit is contained in:
rubenwardy 2023-06-19 21:27:49 +01:00
parent 16f93b3e13
commit 45ed12ddf0
53 changed files with 390 additions and 387 deletions

@ -129,13 +129,13 @@ def check_for_ban():
models.db.session.commit() models.db.session.commit()
from .utils import clearNotifications, is_safe_url, create_session from .utils import clear_notifications, is_safe_url, create_session
@app.before_request @app.before_request
def check_for_notifications(): def check_for_notifications():
if current_user.is_authenticated: if current_user.is_authenticated:
clearNotifications(request.path) clear_notifications(request.path)
@app.errorhandler(404) @app.errorhandler(404)

@ -25,9 +25,9 @@ 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 NotificationType, PackageUpdateConfig, License, UserRank, PackageType
from app.tasks.emails import send_pending_digests from app.tasks.emails import send_pending_digests
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts from app.tasks.forumtasks import import_topic_list, check_all_forum_accounts
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates, updateAllGameSupport from app.tasks.importtasks import import_repo_screenshot, check_zip_release, check_for_updates, update_all_game_support
from app.utils import addNotification, get_system_user from app.utils import add_notification, get_system_user
actions = {} actions = {}
@ -54,13 +54,13 @@ def del_stuck_releases():
@action("Import forum topic list") @action("Import forum topic list")
def import_topic_list(): def import_topic_list():
task = importTopicList.delay() task = import_topic_list.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics"))) return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics")))
@action("Check all forum accounts") @action("Check all forum accounts")
def check_all_forum_accounts(): def check_all_forum_accounts():
task = checkAllForumAccounts.delay() task = check_all_forum_accounts.delay()
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page"))) return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
@ -142,7 +142,7 @@ def remind_wip():
packages_list = _package_list(packages) packages_list = _package_list(packages)
havent = "haven't" if len(packages) > 1 else "hasn't" havent = "haven't" if len(packages) > 1 else "hasn't"
addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL, add_notification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"Did you forget? {packages_list} {havent} been submitted for review yet", f"Did you forget? {packages_list} {havent} been submitted for review yet",
url_for('todo.view_user', username=user.username)) url_for('todo.view_user', username=user.username))
db.session.commit() db.session.commit()
@ -162,7 +162,7 @@ def remind_outdated():
packages = [pkg[0] for pkg in packages] packages = [pkg[0] for pkg in packages]
packages_list = _package_list(packages) packages_list = _package_list(packages)
addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL, add_notification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"The following packages may be outdated: {packages_list}", f"The following packages may be outdated: {packages_list}",
url_for('todo.view_user', username=user.username)) url_for('todo.view_user', username=user.username))
@ -241,7 +241,7 @@ def remind_video_url():
packages = [pkg[0] for pkg in packages] packages = [pkg[0] for pkg in packages]
packages_list = _package_list(packages) packages_list = _package_list(packages)
addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL, add_notification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"You should add a video to {packages_list}", f"You should add a video to {packages_list}",
url_for('users.profile', username=user.username)) url_for('users.profile', username=user.username))
@ -270,7 +270,7 @@ def remind_missing_game_support():
packages = [pkg[0] for pkg in packages] packages = [pkg[0] for pkg in packages]
packages_list = _package_list(packages) packages_list = _package_list(packages)
addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL, add_notification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"You need to confirm whether the following packages support all games: {packages_list}", f"You need to confirm whether the following packages support all games: {packages_list}",
url_for('todo.all_game_support', username=user.username)) url_for('todo.all_game_support', username=user.username))
@ -280,7 +280,7 @@ def remind_missing_game_support():
@action("Detect game support") @action("Detect game support")
def detect_game_support(): def detect_game_support():
task_id = uuid() task_id = uuid()
updateAllGameSupport.apply_async((), task_id=task_id) update_all_game_support.apply_async((), task_id=task_id)
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")))
@ -308,7 +308,7 @@ def check_releases():
tasks = [] tasks = []
for release in releases: for release in releases:
tasks.append(checkZipRelease.s(release.id, release.file_path)) tasks.append(check_zip_release.s(release.id, release.file_path))
result = group(tasks).apply_async() result = group(tasks).apply_async()
@ -325,7 +325,7 @@ def reimport_packages():
for package in Package.query.filter(Package.state != PackageState.DELETED).all(): for package in Package.query.filter(Package.state != PackageState.DELETED).all():
release = package.releases.first() release = package.releases.first()
if release: if release:
tasks.append(checkZipRelease.s(release.id, release.file_path)) tasks.append(check_zip_release.s(release.id, release.file_path))
result = group(tasks).apply_async() result = group(tasks).apply_async()
@ -344,6 +344,6 @@ def import_screenshots():
.filter(PackageScreenshot.id == None) \ .filter(PackageScreenshot.id == None) \
.all() .all()
for package in packages: for package in packages:
importRepoScreenshot.delay(package.id) import_repo_screenshot.delay(package.id)
return redirect(url_for("admin.admin_page")) return redirect(url_for("admin.admin_page"))

@ -19,7 +19,7 @@ from flask_login import current_user, login_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField from wtforms import StringField, SubmitField
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length
from app.utils import rank_required, addAuditLog, addNotification, get_system_user from app.utils import rank_required, add_audit_log, add_notification, get_system_user
from . import bp from . import bp
from .actions import actions from .actions import actions
from app.models import UserRank, Package, db, PackageState, User, AuditSeverity, NotificationType from app.models import UserRank, Package, db, PackageState, User, AuditSeverity, NotificationType
@ -75,11 +75,11 @@ class SendNotificationForm(FlaskForm):
def send_bulk_notification(): def send_bulk_notification():
form = SendNotificationForm(request.form) form = SendNotificationForm(request.form)
if form.validate_on_submit(): if form.validate_on_submit():
addAuditLog(AuditSeverity.MODERATION, current_user, add_audit_log(AuditSeverity.MODERATION, current_user,
"Sent bulk notification", url_for("admin.admin_page"), None, form.title.data) "Sent bulk notification", url_for("admin.admin_page"), None, form.title.data)
users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).all() users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).all()
addNotification(users, get_system_user(), NotificationType.OTHER, form.title.data, form.url.data, None) add_notification(users, get_system_user(), NotificationType.OTHER, form.title.data, form.url.data, None)
db.session.commit() db.session.commit()
return redirect(url_for("admin.admin_page")) return redirect(url_for("admin.admin_page"))
@ -105,7 +105,7 @@ def restore():
else: else:
package.state = target package.state = target
addAuditLog(AuditSeverity.EDITOR, current_user, f"Restored package to state {target.value}", add_audit_log(AuditSeverity.EDITOR, current_user, f"Restored package to state {target.value}",
package.get_url("packages.view"), package) package.get_url("packages.view"), package)
db.session.commit() db.session.commit()

@ -22,7 +22,7 @@ from wtforms.validators import InputRequired, Length
from app.markdown import render_markdown from app.markdown import render_markdown
from app.tasks.emails import send_user_email, send_bulk_email as task_send_bulk from app.tasks.emails import send_user_email, send_bulk_email as task_send_bulk
from app.utils import rank_required, addAuditLog from app.utils import rank_required, add_audit_log
from . import bp from . import bp
from app.models import UserRank, User, AuditSeverity from app.models import UserRank, User, AuditSeverity
@ -49,7 +49,7 @@ def send_single_email():
form = SendEmailForm(request.form) form = SendEmailForm(request.form)
if form.validate_on_submit(): if form.validate_on_submit():
addAuditLog(AuditSeverity.MODERATION, current_user, add_audit_log(AuditSeverity.MODERATION, current_user,
"Sent email to {}".format(user.display_name), url_for("users.profile", username=username)) "Sent email to {}".format(user.display_name), url_for("users.profile", username=username))
text = form.text.data text = form.text.data
@ -65,7 +65,7 @@ def send_single_email():
def send_bulk_email(): def send_bulk_email():
form = SendEmailForm(request.form) form = SendEmailForm(request.form)
if form.validate_on_submit(): if form.validate_on_submit():
addAuditLog(AuditSeverity.MODERATION, current_user, add_audit_log(AuditSeverity.MODERATION, current_user,
"Sent bulk email", url_for("admin.admin_page"), None, form.text.data) "Sent bulk email", url_for("admin.admin_page"), None, form.text.data)
text = form.text.data text = form.text.data

@ -21,7 +21,7 @@ from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SubmitField, URLField from wtforms import StringField, BooleanField, SubmitField, URLField
from wtforms.validators import InputRequired, Length, Optional from wtforms.validators import InputRequired, Length, Optional
from app.utils import rank_required, nonEmptyOrNone, addAuditLog from app.utils import rank_required, nonempty_or_none, add_audit_log
from . import bp from . import bp
from app.models import UserRank, License, db, AuditSeverity from app.models import UserRank, License, db, AuditSeverity
@ -35,7 +35,7 @@ def license_list():
class LicenseForm(FlaskForm): class LicenseForm(FlaskForm):
name = StringField("Name", [InputRequired(), Length(3, 100)]) name = StringField("Name", [InputRequired(), Length(3, 100)])
is_foss = BooleanField("Is FOSS") is_foss = BooleanField("Is FOSS")
url = URLField("URL", [Optional()], filters=[nonEmptyOrNone]) url = URLField("URL", [Optional()], filters=[nonempty_or_none])
submit = SubmitField("Save") submit = SubmitField("Save")
@ -58,12 +58,12 @@ def create_edit_license(name=None):
db.session.add(license) db.session.add(license)
flash("Created license " + form.name.data, "success") flash("Created license " + form.name.data, "success")
addAuditLog(AuditSeverity.MODERATION, current_user, f"Created license {license.name}", add_audit_log(AuditSeverity.MODERATION, current_user, f"Created license {license.name}",
url_for("admin.license_list")) url_for("admin.license_list"))
else: else:
flash("Updated license " + form.name.data, "success") flash("Updated license " + form.name.data, "success")
addAuditLog(AuditSeverity.MODERATION, current_user, f"Edited license {license.name}", add_audit_log(AuditSeverity.MODERATION, current_user, f"Edited license {license.name}",
url_for("admin.license_list")) url_for("admin.license_list"))
form.populate_obj(license) form.populate_obj(license)

@ -23,7 +23,7 @@ from wtforms.validators import InputRequired, Length, Optional, Regexp
from . import bp from . import bp
from app.models import Permission, Tag, db, AuditSeverity from app.models import Permission, Tag, db, AuditSeverity
from app.utils import addAuditLog from app.utils import add_audit_log
@bp.route("/tags/") @bp.route("/tags/")
@ -72,12 +72,12 @@ def create_edit_tag(name=None):
tag.is_protected = form.is_protected.data tag.is_protected = form.is_protected.data
db.session.add(tag) db.session.add(tag)
addAuditLog(AuditSeverity.EDITOR, current_user, f"Created tag {tag.name}", add_audit_log(AuditSeverity.EDITOR, current_user, f"Created tag {tag.name}",
url_for("admin.create_edit_tag", name=tag.name)) url_for("admin.create_edit_tag", name=tag.name))
else: else:
form.populate_obj(tag) form.populate_obj(tag)
addAuditLog(AuditSeverity.EDITOR, current_user, f"Edited tag {tag.name}", add_audit_log(AuditSeverity.EDITOR, current_user, f"Edited tag {tag.name}",
url_for("admin.create_edit_tag", name=tag.name)) url_for("admin.create_edit_tag", name=tag.name))
db.session.commit() db.session.commit()

@ -21,7 +21,7 @@ from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, SubmitField from wtforms import StringField, IntegerField, SubmitField
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length
from app.utils import rank_required, addAuditLog from app.utils import rank_required, add_audit_log
from . import bp from . import bp
from app.models import UserRank, MinetestRelease, db, AuditSeverity from app.models import UserRank, MinetestRelease, db, AuditSeverity
@ -56,12 +56,12 @@ def create_edit_version(name=None):
db.session.add(version) db.session.add(version)
flash("Created version " + form.name.data, "success") flash("Created version " + form.name.data, "success")
addAuditLog(AuditSeverity.MODERATION, current_user, f"Created version {version.name}", add_audit_log(AuditSeverity.MODERATION, current_user, f"Created version {version.name}",
url_for("admin.license_list")) url_for("admin.license_list"))
else: else:
flash("Updated version " + form.name.data, "success") flash("Updated version " + form.name.data, "success")
addAuditLog(AuditSeverity.MODERATION, current_user, f"Edited version {version.name}", add_audit_log(AuditSeverity.MODERATION, current_user, f"Edited version {version.name}",
url_for("admin.version_list")) url_for("admin.version_list"))
form.populate_obj(version) form.populate_obj(version)

@ -30,7 +30,7 @@ from app.markdown import render_markdown
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, ForumTopic, \ from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, ForumTopic, \
MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread
from app.querybuilder import QueryBuilder from app.querybuilder import QueryBuilder
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes, get_request_date from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, is_yes, get_request_date
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, \
@ -66,19 +66,19 @@ def cached(max_age: int):
@cached(300) @cached(300)
def packages(): def packages():
qb = QueryBuilder(request.args) qb = QueryBuilder(request.args)
query = qb.buildPackageQuery() query = qb.build_package_query()
if request.args.get("fmt") == "keys": if request.args.get("fmt") == "keys":
return jsonify([pkg.as_key_dict() for pkg in query.all()]) return jsonify([pkg.as_key_dict() for pkg in query.all()])
pkgs = qb.convertToDictionary(query.all()) pkgs = qb.convert_to_dictionary(query.all())
if "engine_version" in request.args or "protocol_version" in request.args: if "engine_version" in request.args or "protocol_version" in request.args:
pkgs = [pkg for pkg in pkgs if pkg.get("release")] pkgs = [pkg for pkg in pkgs if pkg.get("release")]
# Promote featured packages # Promote featured packages
if "sort" not in request.args and "order" not in request.args and "q" not in request.args: if "sort" not in request.args and "order" not in request.args and "q" not in request.args:
featured_lut = set() featured_lut = set()
featured = qb.convertToDictionary(query.filter(Package.tags.any(name="featured")).all()) featured = qb.convert_to_dictionary(query.filter(Package.tags.any(name="featured")).all())
for pkg in featured: for pkg in featured:
featured_lut.add(f"{pkg['author']}/{pkg['name']}") featured_lut.add(f"{pkg['author']}/{pkg['name']}")
pkg["short_description"] = "Featured. " + pkg["short_description"] pkg["short_description"] = "Featured. " + pkg["short_description"]
@ -101,7 +101,7 @@ def package_view(package):
@cors_allowed @cors_allowed
def package_hypertext(package): def package_hypertext(package):
formspec_version = request.args["formspec_version"] formspec_version = request.args["formspec_version"]
include_images = isYes(request.args.get("include_images", "true")) include_images = is_yes(request.args.get("include_images", "true"))
html = render_markdown(package.desc) html = render_markdown(package.desc)
return jsonify(html_to_minetest(html, formspec_version, include_images)) return jsonify(html_to_minetest(html, formspec_version, include_images))
@ -174,7 +174,7 @@ def package_dependencies(package):
@cors_allowed @cors_allowed
def topics(): def topics():
qb = QueryBuilder(request.args) qb = QueryBuilder(request.args)
query = qb.buildTopicQuery(show_added=True) query = qb.build_topic_query(show_added=True)
return jsonify([t.as_dict() for t in query.all()]) return jsonify([t.as_dict() for t in query.all()])
@ -346,7 +346,7 @@ def create_screenshot(token: APIToken, package: Package):
if file is None: if file is None:
error(400, "Missing 'file' in multipart body") error(400, "Missing 'file' in multipart body")
return api_create_screenshot(token, package, data["title"], file, isYes(data.get("is_cover_image"))) return api_create_screenshot(token, package, data["title"], file, is_yes(data.get("is_cover_image")))
@bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/") @bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/")
@ -454,7 +454,7 @@ def list_all_reviews():
query = query.filter(PackageReview.author.has(User.username == request.args.get("author"))) query = query.filter(PackageReview.author.has(User.username == request.args.get("author")))
if request.args.get("is_positive"): if request.args.get("is_positive"):
if isYes(request.args.get("is_positive")): if is_yes(request.args.get("is_positive")):
query = query.filter(PackageReview.rating > 3) query = query.filter(PackageReview.rating > 3)
else: else:
query = query.filter(PackageReview.rating <= 3) query = query.filter(PackageReview.rating <= 3)
@ -499,7 +499,7 @@ def all_package_stats():
@cached(300) @cached(300)
def package_scores(): def package_scores():
qb = QueryBuilder(request.args) qb = QueryBuilder(request.args)
query = qb.buildPackageQuery() query = qb.build_package_query()
pkgs = [package.as_score_dict() for package in query.all()] pkgs = [package.as_score_dict() for package in query.all()]
return jsonify(pkgs) return jsonify(pkgs)
@ -601,7 +601,7 @@ def versions():
@cors_allowed @cors_allowed
def all_deps(): def all_deps():
qb = QueryBuilder(request.args) qb = QueryBuilder(request.args)
query = qb.buildPackageQuery() query = qb.build_package_query()
def format_pkg(pkg: Package): def format_pkg(pkg: Package):
return { return {

@ -24,7 +24,7 @@ from wtforms.validators import InputRequired, Length
from wtforms_sqlalchemy.fields import QuerySelectField from wtforms_sqlalchemy.fields import QuerySelectField
from app.models import db, User, APIToken, Permission from app.models import db, User, APIToken, Permission
from app.utils import randomString from app.utils import random_string
from . import bp from . import bp
from ..users.settings import get_setting_tabs from ..users.settings import get_setting_tabs
@ -87,7 +87,7 @@ def create_edit_token(username, id=None):
token = APIToken() token = APIToken()
db.session.add(token) db.session.add(token)
token.owner = user token.owner = user
token.access_token = randomString(32) token.access_token = random_string(32)
form.populate_obj(token) form.populate_obj(token)
db.session.commit() db.session.commit()
@ -117,7 +117,7 @@ def reset_token(username, id):
elif token.owner != user: elif token.owner != user:
abort(403) abort(403)
token.access_token = randomString(32) token.access_token = random_string(32)
db.session.commit() # save db.session.commit() # save

@ -24,7 +24,7 @@ from flask_login import current_user
from sqlalchemy import func, or_, and_ from sqlalchemy import func, or_, and_
from app import github, csrf from app import github, csrf
from app.models import db, User, APIToken, Package, Permission, AuditSeverity, PackageState from app.models import db, User, APIToken, Package, Permission, AuditSeverity, PackageState
from app.utils import abs_url_for, addAuditLog, login_user_set_active from app.utils import abs_url_for, add_audit_log, login_user_set_active
from app.blueprints.api.support import error, api_create_vcs_release from app.blueprints.api.support import error, api_create_vcs_release
import hmac, requests import hmac, requests
@ -76,7 +76,7 @@ def callback(oauth_token):
flash(gettext("Authorization failed [err=gh-login-failed]"), "danger") flash(gettext("Authorization failed [err=gh-login-failed]"), "danger")
return redirect(url_for("users.login")) return redirect(url_for("users.login"))
addAuditLog(AuditSeverity.USER, userByGithub, "Logged in using GitHub OAuth", add_audit_log(AuditSeverity.USER, userByGithub, "Logged in using GitHub OAuth",
url_for("users.profile", username=userByGithub.username)) url_for("users.profile", username=userByGithub.username))
db.session.commit() db.session.commit()
return ret return ret

@ -34,7 +34,7 @@ from app.logic.LogicError import LogicError
from app.logic.packages import do_edit_package from app.logic.packages import do_edit_package
from app.querybuilder import QueryBuilder from app.querybuilder import QueryBuilder
from app.rediscache import has_key, set_key from app.rediscache import has_key, set_key
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease from app.tasks.importtasks import import_repo_screenshot, check_zip_release
from app.tasks.webhooktasks import post_discord_webhook from app.tasks.webhooktasks import post_discord_webhook
from app.logic.game_support import GameSupportResolver from app.logic.game_support import GameSupportResolver
@ -43,14 +43,14 @@ from app.models import Package, Tag, db, User, Tags, PackageState, Permission, P
Dependency, Thread, UserRank, PackageReview, PackageDevState, ContentWarning, License, AuditSeverity, \ Dependency, Thread, UserRank, PackageReview, PackageDevState, ContentWarning, License, AuditSeverity, \
PackageScreenshot, NotificationType, AuditLogEntry, PackageAlias, PackageProvides, PackageGameSupport, \ PackageScreenshot, NotificationType, AuditLogEntry, PackageAlias, PackageProvides, PackageGameSupport, \
PackageDailyStats PackageDailyStats
from app.utils import is_user_bot, get_int_or_abort, is_package_page, abs_url_for, addAuditLog, getPackageByInfo, \ from app.utils import is_user_bot, get_int_or_abort, is_package_page, abs_url_for, add_audit_log, get_package_by_info, \
addNotification, get_system_user, rank_required, get_games_from_csv, get_daterange_options add_notification, get_system_user, rank_required, get_games_from_csv, get_daterange_options
@bp.route("/packages/") @bp.route("/packages/")
def list_all(): def list_all():
qb = QueryBuilder(request.args) qb = QueryBuilder(request.args)
query = qb.buildPackageQuery() query = qb.build_package_query()
title = qb.title title = qb.title
query = query.options( query = query.options(
@ -78,7 +78,7 @@ def list_all():
if package: if package:
return redirect(package.get_url("packages.view")) return redirect(package.get_url("packages.view"))
topic = qb.buildTopicQuery().first() topic = qb.build_topic_query().first()
if qb.search and topic: if qb.search and topic:
return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id)) return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
@ -100,12 +100,12 @@ def list_all():
topics = None topics = None
if qb.search and not query.has_next: if qb.search and not query.has_next:
qb.show_discarded = True qb.show_discarded = True
topics = qb.buildTopicQuery().all() topics = qb.build_topic_query().all()
tags_query = db.session.query(func.count(Tags.c.tag_id), Tag) \ tags_query = db.session.query(func.count(Tags.c.tag_id), Tag) \
.select_from(Tag).join(Tags).join(Package).filter(Package.state==PackageState.APPROVED) \ .select_from(Tag).join(Tags).join(Package).filter(Package.state==PackageState.APPROVED) \
.group_by(Tag.id).order_by(db.asc(Tag.title)) .group_by(Tag.id).order_by(db.asc(Tag.title))
tags = qb.filterPackageQuery(tags_query).all() tags = qb.filter_package_query(tags_query).all()
selected_tags = set(qb.tags) selected_tags = set(qb.tags)
@ -115,7 +115,7 @@ def list_all():
authors=authors, packages_count=query.total, topics=topics, noindex=qb.noindex) authors=authors, packages_count=query.total, topics=topics, noindex=qb.noindex)
def getReleases(package): def get_releases(package):
if package.check_perm(current_user, Permission.MAKE_RELEASE): if package.check_perm(current_user, Permission.MAKE_RELEASE):
return package.releases.limit(5) return package.releases.limit(5)
else: else:
@ -158,7 +158,7 @@ def view(package):
Dependency.meta_package_id.in_([p.id for p in package.provides]))) \ Dependency.meta_package_id.in_([p.id for p in package.provides]))) \
.order_by(db.desc(Package.score)).limit(6).all() .order_by(db.desc(Package.score)).limit(6).all()
releases = getReleases(package) releases = get_releases(package)
review_thread = package.review_thread review_thread = package.review_thread
if review_thread is not None and not review_thread.check_perm(current_user, Permission.SEE_THREAD): if review_thread is not None and not review_thread.check_perm(current_user, Permission.SEE_THREAD):
@ -185,7 +185,7 @@ def view(package):
threads = Thread.query.filter_by(package_id=package.id, review_id=None) threads = Thread.query.filter_by(package_id=package.id, review_id=None)
if not current_user.is_authenticated: if not current_user.is_authenticated:
threads = threads.filter_by(private=False) threads = threads.filter_by(private=False)
elif not current_user.rank.atLeast(UserRank.APPROVER) and not current_user == package.author: elif not current_user.rank.at_least(UserRank.APPROVER) and not current_user == package.author:
threads = threads.filter(or_(Thread.private == False, Thread.author == current_user)) threads = threads.filter(or_(Thread.private == False, Thread.author == current_user))
has_review = current_user.is_authenticated and \ has_review = current_user.is_authenticated and \
@ -310,10 +310,10 @@ def handle_create_edit(package: typing.Optional[Package], form: PackageForm, aut
if wasNew: if wasNew:
msg = f"Created package {author.username}/{form.name.data}" msg = f"Created package {author.username}/{form.name.data}"
addAuditLog(AuditSeverity.NORMAL, current_user, msg, package.get_url("packages.view"), package) 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:
importRepoScreenshot.delay(package.id) import_repo_screenshot.delay(package.id)
next_url = package.get_url("packages.view") next_url = package.get_url("packages.view")
if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name): if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
@ -347,7 +347,7 @@ def create_edit(author=None, name=None):
return redirect(url_for("packages.create_edit")) return redirect(url_for("packages.create_edit"))
else: else:
package = getPackageByInfo(author, name) package = get_package_by_info(author, name)
if package is None: if package is None:
abort(404) abort(404)
if not package.check_perm(current_user, Permission.EDIT_PACKAGE): if not package.check_perm(current_user, Permission.EDIT_PACKAGE):
@ -422,9 +422,9 @@ def move_to_state(package):
"Ready for Review: {}".format(package.get_url("packages.view", absolute=True)), True, "Ready for Review: {}".format(package.get_url("packages.view", absolute=True)), True,
package.title, package.short_desc, package.get_thumb_url(2, True)) package.title, package.short_desc, package.get_thumb_url(2, True))
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_APPROVAL, msg, package.get_url("packages.view"), package) add_notification(package.maintainers, current_user, NotificationType.PACKAGE_APPROVAL, msg, package.get_url("packages.view"), package)
severity = AuditSeverity.NORMAL if current_user in package.maintainers else AuditSeverity.EDITOR severity = AuditSeverity.NORMAL if current_user in package.maintainers else AuditSeverity.EDITOR
addAuditLog(severity, current_user, msg, package.get_url("packages.view"), package) add_audit_log(severity, current_user, msg, package.get_url("packages.view"), package)
db.session.commit() db.session.commit()
@ -457,8 +457,8 @@ def remove(package):
url = url_for("users.profile", username=package.author.username) url = url_for("users.profile", username=package.author.username)
msg = "Deleted {}, reason={}".format(package.title, reason) msg = "Deleted {}, reason={}".format(package.title, reason)
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_EDIT, msg, url, package) add_notification(package.maintainers, current_user, NotificationType.PACKAGE_EDIT, msg, url, package)
addAuditLog(AuditSeverity.EDITOR, current_user, msg, url, package) add_audit_log(AuditSeverity.EDITOR, current_user, msg, url, package)
db.session.commit() db.session.commit()
flash(gettext("Deleted package"), "success") flash(gettext("Deleted package"), "success")
@ -472,8 +472,8 @@ def remove(package):
package.state = PackageState.WIP package.state = PackageState.WIP
msg = "Unapproved {}, reason={}".format(package.title, reason) msg = "Unapproved {}, reason={}".format(package.title, reason)
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_APPROVAL, msg, package.get_url("packages.view"), package) add_notification(package.maintainers, current_user, NotificationType.PACKAGE_APPROVAL, msg, package.get_url("packages.view"), package)
addAuditLog(AuditSeverity.EDITOR, current_user, msg, package.get_url("packages.view"), package) add_audit_log(AuditSeverity.EDITOR, current_user, msg, package.get_url("packages.view"), package)
db.session.commit() db.session.commit()
@ -512,12 +512,12 @@ def edit_maintainers(package):
if not user in package.maintainers: if not user in package.maintainers:
if thread: if thread:
thread.watchers.append(user) thread.watchers.append(user)
addNotification(user, current_user, NotificationType.MAINTAINER, add_notification(user, current_user, NotificationType.MAINTAINER,
"Added you as a maintainer of {}".format(package.title), package.get_url("packages.view"), package) "Added you as a maintainer of {}".format(package.title), package.get_url("packages.view"), package)
for user in package.maintainers: for user in package.maintainers:
if user != package.author and not user in users: if user != package.author and not user in users:
addNotification(user, current_user, NotificationType.MAINTAINER, add_notification(user, current_user, NotificationType.MAINTAINER,
"Removed you as a maintainer of {}".format(package.title), package.get_url("packages.view"), package) "Removed you as a maintainer of {}".format(package.title), package.get_url("packages.view"), package)
package.maintainers.clear() package.maintainers.clear()
@ -526,9 +526,9 @@ def edit_maintainers(package):
package.maintainers.append(package.author) package.maintainers.append(package.author)
msg = "Edited {} maintainers".format(package.title) msg = "Edited {} maintainers".format(package.title)
addNotification(package.author, current_user, NotificationType.MAINTAINER, msg, package.get_url("packages.view"), package) add_notification(package.author, current_user, NotificationType.MAINTAINER, msg, package.get_url("packages.view"), package)
severity = AuditSeverity.NORMAL if current_user == package.author else AuditSeverity.MODERATION severity = AuditSeverity.NORMAL if current_user == package.author else AuditSeverity.MODERATION
addAuditLog(severity, current_user, msg, package.get_url("packages.view"), package) add_audit_log(severity, current_user, msg, package.get_url("packages.view"), package)
db.session.commit() db.session.commit()
@ -553,7 +553,7 @@ def remove_self_maintainers(package):
else: else:
package.maintainers.remove(current_user) package.maintainers.remove(current_user)
addNotification(package.author, current_user, NotificationType.MAINTAINER, add_notification(package.author, current_user, NotificationType.MAINTAINER,
"Removed themself as a maintainer of {}".format(package.title), package.get_url("packages.view"), package) "Removed themself as a maintainer of {}".format(package.title), package.get_url("packages.view"), package)
db.session.commit() db.session.commit()
@ -715,7 +715,7 @@ def game_support(package):
package.supports_all_games = form.supports_all_games.data package.supports_all_games = form.supports_all_games.data
addAuditLog(AuditSeverity.NORMAL, current_user, "Edited game support", package.get_url("packages.game_support"), package) add_audit_log(AuditSeverity.NORMAL, current_user, "Edited game support", package.get_url("packages.game_support"), package)
db.session.commit() db.session.commit()
@ -723,7 +723,7 @@ def game_support(package):
release = package.releases.first() release = package.releases.first()
if release: if release:
task_id = uuid() task_id = uuid()
checkZipRelease.apply_async((release.id, release.file_path), task_id=task_id) check_zip_release.apply_async((release.id, release.file_path), task_id=task_id)
next_url = url_for("tasks.check", id=task_id, r=next_url) next_url = url_for("tasks.check", id=task_id, r=next_url)
return redirect(next_url) return redirect(next_url)

@ -27,7 +27,7 @@ from app.models import Package, db, User, PackageState, Permission, UserRank, Pa
PackageRelease, PackageUpdateTrigger, PackageUpdateConfig PackageRelease, PackageUpdateTrigger, PackageUpdateConfig
from app.rediscache import has_key, set_key, make_download_key from app.rediscache import has_key, set_key, make_download_key
from app.tasks.importtasks import check_update_config from app.tasks.importtasks import check_update_config
from app.utils import is_user_bot, is_package_page, nonEmptyOrNone from app.utils import is_user_bot, is_package_page, nonempty_or_none
from . import bp, get_package_tabs from . import bp, get_package_tabs
@ -263,7 +263,7 @@ def set_update_config(package, form):
db.session.add(package.update_config) db.session.add(package.update_config)
form.populate_obj(package.update_config) form.populate_obj(package.update_config)
package.update_config.ref = nonEmptyOrNone(form.ref.data) package.update_config.ref = nonempty_or_none(form.ref.data)
package.update_config.make_release = form.action.data == "make_release" package.update_config.make_release = form.action.data == "make_release"
if package.update_config.trigger == PackageUpdateTrigger.COMMIT: if package.update_config.trigger == PackageUpdateTrigger.COMMIT:
@ -349,7 +349,7 @@ def bulk_update_config(username=None):
if not user: if not user:
abort(404) abort(404)
if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR): if current_user != user and not current_user.rank.at_least(UserRank.EDITOR):
abort(403) abort(403)
form = PackageUpdateConfigFrom() form = PackageUpdateConfigFrom()

@ -26,8 +26,8 @@ from wtforms.validators import InputRequired, Length
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \ from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \
Permission, AuditSeverity, PackageState Permission, AuditSeverity, PackageState
from app.tasks.webhooktasks import post_discord_webhook from app.tasks.webhooktasks import post_discord_webhook
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, \ from app.utils import is_package_page, add_notification, get_int_or_abort, is_yes, is_safe_url, rank_required, \
addAuditLog, has_blocked_domains add_audit_log, has_blocked_domains
from . import bp from . import bp
@ -123,7 +123,7 @@ def review(package):
notif_msg = "Updated review '{}'".format(form.title.data) notif_msg = "Updated review '{}'".format(form.title.data)
type = NotificationType.OTHER type = NotificationType.OTHER
addNotification(package.maintainers, current_user, type, notif_msg, add_notification(package.maintainers, current_user, type, notif_msg,
url_for("threads.view", id=thread.id), package) url_for("threads.view", id=thread.id), package)
if was_new: if was_new:
@ -163,11 +163,11 @@ def delete_review(package, reviewer):
thread.review = None thread.review = None
msg = "Converted review by {} to thread".format(review.author.display_name) msg = "Converted review by {} to thread".format(review.author.display_name)
addAuditLog(AuditSeverity.MODERATION if current_user.username != reviewer else AuditSeverity.NORMAL, add_audit_log(AuditSeverity.MODERATION if current_user.username != reviewer else AuditSeverity.NORMAL,
current_user, msg, thread.get_view_url(), thread.package) current_user, msg, thread.get_view_url(), thread.package)
notif_msg = "Deleted review '{}', comments were kept as a thread".format(thread.title) notif_msg = "Deleted review '{}', comments were kept as a thread".format(thread.title)
addNotification(package.maintainers, current_user, NotificationType.OTHER, notif_msg, url_for("threads.view", id=thread.id), package) add_notification(package.maintainers, current_user, NotificationType.OTHER, notif_msg, url_for("threads.view", id=thread.id), package)
db.session.delete(review) db.session.delete(review)
@ -191,7 +191,7 @@ def handle_review_vote(package: Package, review_id: int):
flash(gettext("You can't vote on your own reviews!"), "danger") flash(gettext("You can't vote on your own reviews!"), "danger")
return return
is_positive = isYes(request.form["is_positive"]) is_positive = is_yes(request.form["is_positive"])
vote = PackageReviewVote.query.filter_by(review=review, user=current_user).first() vote = PackageReviewVote.query.filter_by(review=review, user=current_user).first()
if vote is None: if vote is None:

@ -25,7 +25,7 @@ from wtforms.validators import InputRequired, Length
from app.models import User, UserRank from app.models import User, UserRank
from app.tasks.emails import send_user_email 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 isNo, abs_url_samesite from app.utils import is_no, abs_url_samesite
bp = Blueprint("report", __name__) bp = Blueprint("report", __name__)
@ -37,7 +37,7 @@ class ReportForm(FlaskForm):
@bp.route("/report/", methods=["GET", "POST"]) @bp.route("/report/", methods=["GET", "POST"])
def report(): def report():
is_anon = not current_user.is_authenticated or not isNo(request.args.get("anon")) is_anon = not current_user.is_authenticated or not is_no(request.args.get("anon"))
url = request.args.get("url") url = request.args.get("url")
if url: if url:

@ -20,8 +20,8 @@ from flask_login import login_required, current_user
from app import csrf from app import csrf
from app.models import UserRank from app.models import UserRank
from app.tasks import celery from app.tasks import celery
from app.tasks.importtasks import getMeta from app.tasks.importtasks import get_meta
from app.utils import shouldReturnJson from app.utils import should_return_json
bp = Blueprint("tasks", __name__) bp = Blueprint("tasks", __name__)
@ -33,7 +33,7 @@ def start_getmeta():
from flask import request from flask import request
author = request.args.get("author") author = request.args.get("author")
author = current_user.forums_username if author is None else author author = current_user.forums_username if author is None else author
aresult = getMeta.delay(request.args.get("url"), author) aresult = get_meta.delay(request.args.get("url"), author)
return jsonify({ return jsonify({
"poll_url": url_for("tasks.check", id=aresult.id), "poll_url": url_for("tasks.check", id=aresult.id),
}) })
@ -52,7 +52,7 @@ def check(id):
'status': status, 'status': status,
} }
if current_user.is_authenticated and current_user.rank.atLeast(UserRank.ADMIN): if current_user.is_authenticated and current_user.rank.at_least(UserRank.ADMIN):
info["error"] = str(traceback) info["error"] = str(traceback)
elif str(result)[1:12] == "TaskError: ": elif str(result)[1:12] == "TaskError: ":
info["error"] = str(result)[12:-1] info["error"] = str(result)[12:-1]
@ -65,7 +65,7 @@ def check(id):
'result': result, 'result': result,
} }
if shouldReturnJson(): if should_return_json():
return jsonify(info) return jsonify(info)
else: else:
r = request.args.get("r") r = request.args.get("r")

@ -25,7 +25,7 @@ bp = Blueprint("threads", __name__)
from flask_login import current_user, login_required from flask_login import current_user, login_required
from app.models import Package, db, User, Permission, Thread, UserRank, AuditSeverity, \ from app.models import Package, db, User, Permission, Thread, UserRank, AuditSeverity, \
NotificationType, ThreadReply NotificationType, ThreadReply
from app.utils import addNotification, isYes, addAuditLog, get_system_user, rank_required, has_blocked_domains from app.utils import add_notification, is_yes, add_audit_log, get_system_user, rank_required, has_blocked_domains
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, BooleanField from wtforms import StringField, TextAreaField, SubmitField, BooleanField
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length
@ -99,7 +99,7 @@ def set_lock(id_):
if thread is None or not thread.check_perm(current_user, Permission.LOCK_THREAD): if thread is None or not thread.check_perm(current_user, Permission.LOCK_THREAD):
abort(404) abort(404)
thread.locked = isYes(request.args.get("lock")) thread.locked = is_yes(request.args.get("lock"))
if thread.locked is None: if thread.locked is None:
abort(400) abort(400)
@ -110,8 +110,8 @@ def set_lock(id_):
msg = "Unlocked thread '{}'".format(thread.title) msg = "Unlocked thread '{}'".format(thread.title)
flash(gettext("Unlocked thread"), "success") flash(gettext("Unlocked thread"), "success")
addNotification(thread.watchers, current_user, NotificationType.OTHER, msg, thread.get_view_url(), thread.package) add_notification(thread.watchers, current_user, NotificationType.OTHER, msg, thread.get_view_url(), thread.package)
addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.get_view_url(), thread.package) add_audit_log(AuditSeverity.MODERATION, current_user, msg, thread.get_view_url(), thread.package)
db.session.commit() db.session.commit()
@ -134,7 +134,7 @@ def delete_thread(id_):
db.session.delete(thread) db.session.delete(thread)
addAuditLog(AuditSeverity.MODERATION, current_user, msg, None, thread.package, summary) add_audit_log(AuditSeverity.MODERATION, current_user, msg, None, thread.package, summary)
db.session.commit() db.session.commit()
@ -167,7 +167,7 @@ def delete_reply(id_):
return render_template("threads/delete_reply.html", thread=thread, reply=reply) return render_template("threads/delete_reply.html", thread=thread, reply=reply)
msg = "Deleted reply by {}".format(reply.author.display_name) msg = "Deleted reply by {}".format(reply.author.display_name)
addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.get_view_url(), thread.package, reply.comment) add_audit_log(AuditSeverity.MODERATION, current_user, msg, thread.get_view_url(), thread.package, reply.comment)
db.session.delete(reply) db.session.delete(reply)
db.session.commit() db.session.commit()
@ -206,8 +206,8 @@ def edit_reply(id_):
else: else:
msg = "Edited reply by {}".format(reply.author.display_name) msg = "Edited reply by {}".format(reply.author.display_name)
severity = AuditSeverity.NORMAL if current_user == reply.author else AuditSeverity.MODERATION severity = AuditSeverity.NORMAL if current_user == reply.author else AuditSeverity.MODERATION
addNotification(reply.author, current_user, NotificationType.OTHER, msg, thread.get_view_url(), thread.package) add_notification(reply.author, current_user, NotificationType.OTHER, msg, thread.get_view_url(), thread.package)
addAuditLog(severity, current_user, msg, thread.get_view_url(), thread.package, reply.comment) add_audit_log(severity, current_user, msg, thread.get_view_url(), thread.package, reply.comment)
reply.comment = comment reply.comment = comment
@ -253,17 +253,17 @@ def view(id_):
continue continue
msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title) msg = "Mentioned by {} in '{}'".format(current_user.display_name, thread.title)
addNotification(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)
thread.watchers.append(mentioned) thread.watchers.append(mentioned)
msg = "New comment on '{}'".format(thread.title) msg = "New comment on '{}'".format(thread.title)
addNotification(thread.watchers, current_user, NotificationType.THREAD_REPLY, msg, thread.get_view_url(), thread.package) add_notification(thread.watchers, current_user, NotificationType.THREAD_REPLY, msg, thread.get_view_url(), thread.package)
if thread.author == get_system_user(): if thread.author == get_system_user():
approvers = User.query.filter(User.rank >= UserRank.APPROVER).all() approvers = User.query.filter(User.rank >= UserRank.APPROVER).all()
addNotification(approvers, current_user, NotificationType.EDITOR_MISC, msg, add_notification(approvers, current_user, NotificationType.EDITOR_MISC, msg,
thread.get_view_url(), thread.package) thread.get_view_url(), thread.package)
post_discord_webhook.delay(current_user.username, post_discord_webhook.delay(current_user.username,
"Replied to bot messages: {}".format(thread.get_view_url(absolute=True)), True) "Replied to bot messages: {}".format(thread.get_view_url(absolute=True)), True)
@ -294,7 +294,7 @@ def new():
abort(404) abort(404)
def_is_private = request.args.get("private") or False def_is_private = request.args.get("private") or False
if package is None and not current_user.rank.atLeast(UserRank.APPROVER): if package is None and not current_user.rank.at_least(UserRank.APPROVER):
abort(404) abort(404)
allow_private_change = not package or package.approved allow_private_change = not package or package.approved
@ -359,17 +359,17 @@ def new():
continue continue
msg = "Mentioned by {} in new thread '{}'".format(current_user.display_name, thread.title) msg = "Mentioned by {} in new thread '{}'".format(current_user.display_name, thread.title)
addNotification(mentioned, current_user, NotificationType.NEW_THREAD, add_notification(mentioned, current_user, NotificationType.NEW_THREAD,
msg, thread.get_view_url(), thread.package) msg, thread.get_view_url(), thread.package)
thread.watchers.append(mentioned) thread.watchers.append(mentioned)
notif_msg = "New thread '{}'".format(thread.title) notif_msg = "New thread '{}'".format(thread.title)
if package is not None: if package is not None:
addNotification(package.maintainers, current_user, NotificationType.NEW_THREAD, notif_msg, thread.get_view_url(), package) add_notification(package.maintainers, current_user, NotificationType.NEW_THREAD, notif_msg, thread.get_view_url(), package)
approvers = User.query.filter(User.rank >= UserRank.APPROVER).all() approvers = User.query.filter(User.rank >= UserRank.APPROVER).all()
addNotification(approvers, current_user, NotificationType.EDITOR_MISC, notif_msg, thread.get_view_url(), package) add_notification(approvers, current_user, NotificationType.EDITOR_MISC, notif_msg, thread.get_view_url(), package)
if is_review_thread: if is_review_thread:
post_discord_webhook.delay(current_user.username, post_discord_webhook.delay(current_user.username,

@ -22,7 +22,7 @@ from sqlalchemy import or_
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, MinetestRelease 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, isYes from app.utils import get_int_or_abort, is_yes
from . import bp from . import bp
@ -85,7 +85,7 @@ def view_editor():
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,
canApproveNew=can_approve_new, canApproveRel=can_approve_rel, canApproveScn=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) unfulfilled_meta_packages=unfulfilled_meta_packages, audit_log=audit_log)
@ -94,8 +94,8 @@ def view_editor():
@login_required @login_required
def topics(): def topics():
qb = QueryBuilder(request.args) qb = QueryBuilder(request.args)
qb.setSortIfNone("date") qb.set_sort_if_none("date")
query = qb.buildTopicQuery() query = qb.build_topic_query()
tmp_q = ForumTopic.query tmp_q = ForumTopic.query
if not qb.show_discarded: if not qb.show_discarded:
@ -105,7 +105,7 @@ def topics():
page = get_int_or_abort(request.args.get("page"), 1) page = get_int_or_abort(request.args.get("page"), 1)
num = get_int_or_abort(request.args.get("n"), 100) num = get_int_or_abort(request.args.get("n"), 100)
if num > 100 and not current_user.rank.atLeast(UserRank.APPROVER): if num > 100 and not current_user.rank.at_least(UserRank.APPROVER):
num = 100 num = 100
query = query.paginate(page=page, per_page=num) query = query.paginate(page=page, per_page=num)
@ -126,10 +126,10 @@ def topics():
@login_required @login_required
def tags(): def tags():
qb = QueryBuilder(request.args) qb = QueryBuilder(request.args)
qb.setSortIfNone("score", "desc") qb.set_sort_if_none("score", "desc")
query = qb.buildPackageQuery() query = qb.build_package_query()
only_no_tags = isYes(request.args.get("no_tags")) only_no_tags = is_yes(request.args.get("no_tags"))
if only_no_tags: if only_no_tags:
query = query.filter(Package.tags == None) query = query.filter(Package.tags == None)
@ -153,7 +153,7 @@ def modnames():
@bp.route("/todo/outdated/") @bp.route("/todo/outdated/")
@login_required @login_required
def outdated(): def outdated():
is_mtm_only = isYes(request.args.get("mtm")) is_mtm_only = is_yes(request.args.get("mtm"))
query = db.session.query(Package).select_from(PackageUpdateConfig) \ query = db.session.query(Package).select_from(PackageUpdateConfig) \
.filter(PackageUpdateConfig.outdated_at.isnot(None)) \ .filter(PackageUpdateConfig.outdated_at.isnot(None)) \
@ -177,7 +177,7 @@ def outdated():
@bp.route("/todo/screenshots/") @bp.route("/todo/screenshots/")
@login_required @login_required
def screenshots(): def screenshots():
is_mtm_only = isYes(request.args.get("mtm")) is_mtm_only = is_yes(request.args.get("mtm"))
query = db.session.query(Package) \ query = db.session.query(Package) \
.filter(~Package.screenshots.any()) \ .filter(~Package.screenshots.any()) \
@ -200,7 +200,7 @@ def screenshots():
@bp.route("/todo/mtver_support/") @bp.route("/todo/mtver_support/")
@login_required @login_required
def mtver_support(): def mtver_support():
is_mtm_only = isYes(request.args.get("mtm")) is_mtm_only = is_yes(request.args.get("mtm"))
current_stable = MinetestRelease.query.filter(~MinetestRelease.name.like("%-dev")).order_by(db.desc(MinetestRelease.id)).first() current_stable = MinetestRelease.query.filter(~MinetestRelease.name.like("%-dev")).order_by(db.desc(MinetestRelease.id)).first()

@ -22,8 +22,8 @@ from sqlalchemy import or_, and_
from app.models import User, Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \ from app.models import User, Package, PackageState, PackageScreenshot, PackageUpdateConfig, ForumTopic, db, \
PackageRelease, Permission, NotificationType, AuditSeverity, UserRank, PackageType PackageRelease, Permission, NotificationType, AuditSeverity, UserRank, PackageType
from app.tasks.importtasks import makeVCSRelease from app.tasks.importtasks import make_vcs_release
from app.utils import addNotification, addAuditLog from app.utils import add_notification, add_audit_log
from . import bp from . import bp
@ -43,7 +43,7 @@ def view_user(username=None):
if not user: if not user:
abort(404) abort(404)
if current_user != user and not current_user.rank.atLeast(UserRank.APPROVER): if current_user != user and not current_user.rank.at_least(UserRank.APPROVER):
abort(403) abort(403)
unapproved_packages = user.packages \ unapproved_packages = user.packages \
@ -97,7 +97,7 @@ def apply_all_updates(username):
if not user: if not user:
abort(404) abort(404)
if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR): if current_user != user and not current_user.rank.at_least(UserRank.EDITOR):
abort(403) abort(403)
outdated_packages = user.maintained_packages \ outdated_packages = user.maintained_packages \
@ -124,13 +124,13 @@ def apply_all_updates(username):
db.session.add(rel) db.session.add(rel)
db.session.commit() db.session.commit()
makeVCSRelease.apply_async((rel.id, ref), make_vcs_release.apply_async((rel.id, ref),
task_id=rel.task_id) task_id=rel.task_id)
msg = "Created release {} (Applied all Git Update Detection)".format(rel.title) msg = "Created release {} (Applied all Git Update Detection)".format(rel.title)
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_EDIT, msg, add_notification(package.maintainers, current_user, NotificationType.PACKAGE_EDIT, msg,
rel.get_url("packages.create_edit"), package) rel.get_url("packages.create_edit"), package)
addAuditLog(AuditSeverity.NORMAL, current_user, msg, package.get_url("packages.view"), package) add_audit_log(AuditSeverity.NORMAL, current_user, msg, package.get_url("packages.view"), package)
db.session.commit() db.session.commit()
return redirect(url_for("todo.view_user", username=username)) return redirect(url_for("todo.view_user", username=username))
@ -144,7 +144,7 @@ def all_game_support(username=None):
return redirect(url_for("todo.all_game_support", username=current_user.username)) return redirect(url_for("todo.all_game_support", username=current_user.username))
user: User = User.query.filter_by(username=username).one_or_404() user: User = User.query.filter_by(username=username).one_or_404()
if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR): if current_user != user and not current_user.rank.at_least(UserRank.EDITOR):
abort(403) abort(403)
packages = user.maintained_packages.filter( packages = user.maintained_packages.filter(
@ -159,7 +159,7 @@ def all_game_support(username=None):
@login_required @login_required
def confirm_supports_all_games(username=None): def confirm_supports_all_games(username=None):
user: User = User.query.filter_by(username=username).one_or_404() user: User = User.query.filter_by(username=username).one_or_404()
if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR): if current_user != user and not current_user.rank.at_least(UserRank.EDITOR):
abort(403) abort(403)
packages = user.maintained_packages.filter( packages = user.maintained_packages.filter(
@ -173,7 +173,7 @@ def confirm_supports_all_games(username=None):
package.supports_all_games = True package.supports_all_games = True
db.session.merge(package) db.session.merge(package)
addAuditLog(AuditSeverity.NORMAL, current_user, "Enabled 'Supports all games' (bulk)", add_audit_log(AuditSeverity.NORMAL, current_user, "Enabled 'Supports all games' (bulk)",
package.get_url("packages.game_support"), package) package.get_url("packages.game_support"), package)
db.session.commit() db.session.commit()

@ -25,8 +25,8 @@ from wtforms import StringField, SubmitField, BooleanField, PasswordField, valid
from wtforms.validators import InputRequired, Length, Regexp, DataRequired, Optional, Email, EqualTo from wtforms.validators import InputRequired, Length, Regexp, DataRequired, Optional, Email, EqualTo
from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email
from app.utils import randomString, make_flask_login_password, is_safe_url, check_password_hash, addAuditLog, \ from app.utils import random_string, make_flask_login_password, is_safe_url, check_password_hash, add_audit_log, \
nonEmptyOrNone, post_login, is_username_valid nonempty_or_none, post_login, is_username_valid
from . import bp from . import bp
from app.models import User, AuditSeverity, db, UserRank, PackageAlias, EmailSubscription, UserNotificationPreferences, \ from app.models import User, AuditSeverity, db, UserRank, PackageAlias, EmailSubscription, UserNotificationPreferences, \
UserEmailVerification UserEmailVerification
@ -58,7 +58,7 @@ def handle_login(form):
flash(gettext("You need to confirm the registration email"), "danger") flash(gettext("You need to confirm the registration email"), "danger")
return return
addAuditLog(AuditSeverity.USER, user, "Logged in using password", add_audit_log(AuditSeverity.USER, user, "Logged in using password",
url_for("users.profile", username=user.username)) url_for("users.profile", username=user.username))
db.session.commit() db.session.commit()
@ -97,7 +97,7 @@ def logout():
class RegisterForm(FlaskForm): class RegisterForm(FlaskForm):
display_name = StringField(lazy_gettext("Display Name"), [Optional(), Length(1, 20)], filters=[nonEmptyOrNone]) display_name = StringField(lazy_gettext("Display Name"), [Optional(), Length(1, 20)], filters=[nonempty_or_none])
username = StringField(lazy_gettext("Username"), [InputRequired(), username = StringField(lazy_gettext("Username"), [InputRequired(),
Regexp("^[a-zA-Z0-9._-]+$", message=lazy_gettext( Regexp("^[a-zA-Z0-9._-]+$", message=lazy_gettext(
"Only alphabetic letters (A-Za-z), numbers (0-9), underscores (_), minuses (-), and periods (.) allowed"))]) "Only alphabetic letters (A-Za-z), numbers (0-9), underscores (_), minuses (-), and periods (.) allowed"))])
@ -154,10 +154,10 @@ def handle_register(form):
user.display_name = form.display_name.data user.display_name = form.display_name.data
db.session.add(user) db.session.add(user)
addAuditLog(AuditSeverity.USER, user, "Registered with email, display name=" + user.display_name, add_audit_log(AuditSeverity.USER, user, "Registered with email, display name=" + user.display_name,
url_for("users.profile", username=user.username)) url_for("users.profile", username=user.username))
token = randomString(32) token = random_string(32)
ver = UserEmailVerification() ver = UserEmailVerification()
ver.user = user ver.user = user
@ -194,9 +194,9 @@ def forgot_password():
email = form.email.data email = form.email.data
user = User.query.filter_by(email=email).first() user = User.query.filter_by(email=email).first()
if user: if user:
token = randomString(32) token = random_string(32)
addAuditLog(AuditSeverity.USER, user, "(Anonymous) requested a password reset", add_audit_log(AuditSeverity.USER, user, "(Anonymous) requested a password reset",
url_for("users.profile", username=user.username), None) url_for("users.profile", username=user.username), None)
ver = UserEmailVerification() ver = UserEmailVerification()
@ -241,12 +241,12 @@ def handle_set_password(form):
flash(gettext("Passwords do not match"), "danger") flash(gettext("Passwords do not match"), "danger")
return return
addAuditLog(AuditSeverity.USER, current_user, "Changed their password", url_for("users.profile", username=current_user.username)) add_audit_log(AuditSeverity.USER, current_user, "Changed their password", url_for("users.profile", username=current_user.username))
current_user.password = make_flask_login_password(form.password.data) current_user.password = make_flask_login_password(form.password.data)
if hasattr(form, "email"): if hasattr(form, "email"):
new_email = nonEmptyOrNone(form.email.data) new_email = nonempty_or_none(form.email.data)
if new_email and new_email != current_user.email: if new_email and new_email != current_user.email:
if EmailSubscription.query.filter_by(email=form.email.data, blacklisted=True).count() > 0: if EmailSubscription.query.filter_by(email=form.email.data, blacklisted=True).count() > 0:
flash(gettext(u"That email address has been unsubscribed/blacklisted, and cannot be used"), "danger") flash(gettext(u"That email address has been unsubscribed/blacklisted, and cannot be used"), "danger")
@ -258,7 +258,7 @@ def handle_set_password(form):
gettext(u"We were unable to create the account as the email is already in use by %(display_name)s. Try a different email address.", gettext(u"We were unable to create the account as the email is already in use by %(display_name)s. Try a different email address.",
display_name=user_by_email.display_name)) display_name=user_by_email.display_name))
else: else:
token = randomString(32) token = random_string(32)
ver = UserEmailVerification() ver = UserEmailVerification()
ver.user = current_user ver.user = current_user
@ -329,7 +329,7 @@ def verify_email():
user = ver.user user = ver.user
addAuditLog(AuditSeverity.USER, user, "Confirmed their email", add_audit_log(AuditSeverity.USER, user, "Confirmed their email",
url_for("users.profile", username=user.username)) url_for("users.profile", username=user.username))
was_activating = not user.is_active was_activating = not user.is_active
@ -383,7 +383,7 @@ def unsubscribe_verify():
sub = EmailSubscription(email) sub = EmailSubscription(email)
db.session.add(sub) db.session.add(sub)
sub.token = randomString(32) sub.token = random_string(32)
db.session.commit() db.session.commit()
send_unsubscribe_verify.delay(form.email.data, get_locale().language) send_unsubscribe_verify.delay(form.email.data, get_locale().language)

@ -19,9 +19,9 @@ from flask_babel import gettext
from . import bp from . import bp
from flask import redirect, render_template, session, request, flash, url_for from flask import redirect, render_template, session, request, flash, url_for
from app.models import db, User, UserRank from app.models import db, User, UserRank
from app.utils import randomString, login_user_set_active, is_username_valid from app.utils import random_string, login_user_set_active, is_username_valid
from app.tasks.forumtasks import checkForumAccount from app.tasks.forumtasks import check_forum_account
from app.utils.phpbbparser import getProfile from app.utils.phpbbparser import get_profile
@bp.route("/user/claim/", methods=["GET", "POST"]) @bp.route("/user/claim/", methods=["GET", "POST"])
@ -42,7 +42,7 @@ def claim_forums():
return redirect(url_for("users.claim_forums")) return redirect(url_for("users.claim_forums"))
user = User.query.filter_by(forums_username=username).first() user = User.query.filter_by(forums_username=username).first()
if user and user.rank.atLeast(UserRank.NEW_MEMBER): if user and user.rank.at_least(UserRank.NEW_MEMBER):
flash(gettext("User has already been claimed"), "danger") flash(gettext("User has already been claimed"), "danger")
return redirect(url_for("users.claim_forums")) return redirect(url_for("users.claim_forums"))
elif method == "github": elif method == "github":
@ -55,7 +55,7 @@ def claim_forums():
if "forum_token" in session: if "forum_token" in session:
token = session["forum_token"] token = session["forum_token"]
else: else:
token = randomString(12) token = random_string(12)
session["forum_token"] = token session["forum_token"] = token
if request.method == "POST": if request.method == "POST":
@ -65,17 +65,17 @@ def claim_forums():
if not is_username_valid(username): if not is_username_valid(username):
flash(gettext("Invalid username, Only alphabetic letters (A-Za-z), numbers (0-9), underscores (_), minuses (-), and periods (.) allowed. Consider contacting an admin"), "danger") flash(gettext("Invalid username, Only alphabetic letters (A-Za-z), numbers (0-9), underscores (_), minuses (-), and periods (.) allowed. Consider contacting an admin"), "danger")
elif ctype == "github": elif ctype == "github":
task = checkForumAccount.delay(username) task = check_forum_account.delay(username)
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.claim_forums", username=username, method="github"))) return redirect(url_for("tasks.check", id=task.id, r=url_for("users.claim_forums", username=username, method="github")))
elif ctype == "forum": elif ctype == "forum":
user = User.query.filter_by(forums_username=username).first() user = User.query.filter_by(forums_username=username).first()
if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER): if user is not None and user.rank.at_least(UserRank.NEW_MEMBER):
flash(gettext("That user has already been claimed!"), "danger") flash(gettext("That user has already been claimed!"), "danger")
return redirect(url_for("users.claim_forums")) return redirect(url_for("users.claim_forums"))
# Get signature # Get signature
try: try:
profile = getProfile("https://forum.minetest.net", username) profile = get_profile("https://forum.minetest.net", username)
sig = profile.signature if profile else None sig = profile.signature if profile else None
except IOError as e: except IOError as e:
if hasattr(e, 'message'): if hasattr(e, 'message'):

@ -25,7 +25,7 @@ from wtforms.validators import Length, Optional, Email, URL
from app.models import User, AuditSeverity, db, UserRank, PackageAlias, EmailSubscription, UserNotificationPreferences, \ from app.models import User, AuditSeverity, db, UserRank, PackageAlias, EmailSubscription, UserNotificationPreferences, \
UserEmailVerification, Permission, NotificationType, UserBan UserEmailVerification, Permission, NotificationType, UserBan
from app.tasks.emails import send_verify_email from app.tasks.emails import send_verify_email
from app.utils import nonEmptyOrNone, addAuditLog, randomString, rank_required, has_blocked_domains from app.utils import nonempty_or_none, add_audit_log, random_string, rank_required, has_blocked_domains
from . import bp from . import bp
@ -53,7 +53,7 @@ def get_setting_tabs(user):
}, },
] ]
if current_user.rank.atLeast(UserRank.MODERATOR): if current_user.rank.at_least(UserRank.MODERATOR):
ret.append({ ret.append({
"id": "modtools", "id": "modtools",
"title": gettext("Moderator Tools"), "title": gettext("Moderator Tools"),
@ -64,7 +64,7 @@ def get_setting_tabs(user):
class UserProfileForm(FlaskForm): class UserProfileForm(FlaskForm):
display_name = StringField(lazy_gettext("Display Name"), [Optional(), Length(1, 20)], filters=[lambda x: nonEmptyOrNone(x.strip())]) display_name = StringField(lazy_gettext("Display Name"), [Optional(), Length(1, 20)], filters=[lambda x: nonempty_or_none(x.strip())])
website_url = StringField(lazy_gettext("Website URL"), [Optional(), URL()], filters = [lambda x: x or None]) website_url = StringField(lazy_gettext("Website URL"), [Optional(), URL()], filters = [lambda x: x or None])
donate_url = StringField(lazy_gettext("Donation URL"), [Optional(), URL()], filters = [lambda x: x or None]) donate_url = StringField(lazy_gettext("Donation URL"), [Optional(), URL()], filters = [lambda x: x or None])
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
@ -72,7 +72,7 @@ class UserProfileForm(FlaskForm):
def handle_profile_edit(form: UserProfileForm, user: User, username: str): def handle_profile_edit(form: UserProfileForm, user: User, username: str):
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
addAuditLog(severity, current_user, "Edited {}'s profile".format(user.display_name), add_audit_log(severity, current_user, "Edited {}'s profile".format(user.display_name),
url_for("users.profile", username=username)) url_for("users.profile", username=username))
display_name = form.display_name.data or user.username display_name = form.display_name.data or user.username
@ -95,7 +95,7 @@ def handle_profile_edit(form: UserProfileForm, user: User, username: str):
user.display_name = display_name user.display_name = display_name
severity = AuditSeverity.USER if current_user == user else AuditSeverity.MODERATION severity = AuditSeverity.USER if current_user == user else AuditSeverity.MODERATION
addAuditLog(severity, current_user, "Changed display name of {} to {}" add_audit_log(severity, current_user, "Changed display name of {} to {}"
.format(user.username, user.display_name), .format(user.username, user.display_name),
url_for("users.profile", username=username)) url_for("users.profile", username=username))
@ -167,12 +167,12 @@ def handle_email_notifications(user, prefs: UserNotificationPreferences, is_new,
flash(gettext("That email address has been unsubscribed/blacklisted, and cannot be used"), "danger") flash(gettext("That email address has been unsubscribed/blacklisted, and cannot be used"), "danger")
return return
token = randomString(32) token = random_string(32)
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
msg = "Changed email of {}".format(user.display_name) msg = "Changed email of {}".format(user.display_name)
addAuditLog(severity, current_user, msg, url_for("users.profile", username=user.username)) add_audit_log(severity, current_user, msg, url_for("users.profile", username=user.username))
ver = UserEmailVerification() ver = UserEmailVerification()
ver.user = user ver.user = user
@ -245,19 +245,19 @@ def delete(username):
if not user: if not user:
abort(404) abort(404)
if user.rank.atLeast(UserRank.MODERATOR): if user.rank.at_least(UserRank.MODERATOR):
flash(gettext("Users with moderator rank or above cannot be deleted"), "danger") flash(gettext("Users with moderator rank or above cannot be deleted"), "danger")
return redirect(url_for("users.account", username=username)) return redirect(url_for("users.account", username=username))
if request.method == "GET": if request.method == "GET":
return render_template("users/delete.html", user=user, can_delete=user.can_delete()) return render_template("users/delete.html", user=user, can_delete=user.can_delete())
if "delete" in request.form and (user.can_delete() or current_user.rank.atLeast(UserRank.ADMIN)): if "delete" in request.form and (user.can_delete() or current_user.rank.at_least(UserRank.ADMIN)):
msg = "Deleted user {}".format(user.username) msg = "Deleted user {}".format(user.username)
flash(msg, "success") flash(msg, "success")
addAuditLog(AuditSeverity.MODERATION, current_user, msg, None) add_audit_log(AuditSeverity.MODERATION, current_user, msg, None)
if current_user.rank.atLeast(UserRank.ADMIN): if current_user.rank.at_least(UserRank.ADMIN):
for pkg in user.packages.all(): for pkg in user.packages.all():
pkg.review_thread = None pkg.review_thread = None
db.session.delete(pkg) db.session.delete(pkg)
@ -273,7 +273,7 @@ def delete(username):
msg = "Deactivated user {}".format(user.username) msg = "Deactivated user {}".format(user.username)
flash(msg, "success") flash(msg, "success")
addAuditLog(AuditSeverity.MODERATION, current_user, msg, None) add_audit_log(AuditSeverity.MODERATION, current_user, msg, None)
else: else:
assert False assert False
@ -308,7 +308,7 @@ def modtools(username):
form = ModToolsForm(obj=user) form = ModToolsForm(obj=user)
if form.validate_on_submit(): if form.validate_on_submit():
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
addAuditLog(severity, current_user, "Edited {}'s account".format(user.display_name), add_audit_log(severity, current_user, "Edited {}'s account".format(user.display_name),
url_for("users.profile", username=username)) url_for("users.profile", username=username))
# Copy form fields to user_profile fields # Copy form fields to user_profile fields
@ -322,16 +322,16 @@ def modtools(username):
user.username = form.username.data user.username = form.username.data
user.display_name = form.display_name.data user.display_name = form.display_name.data
user.forums_username = nonEmptyOrNone(form.forums_username.data) user.forums_username = nonempty_or_none(form.forums_username.data)
user.github_username = nonEmptyOrNone(form.github_username.data) user.github_username = nonempty_or_none(form.github_username.data)
if user.check_perm(current_user, Permission.CHANGE_RANK): if user.check_perm(current_user, Permission.CHANGE_RANK):
new_rank = form["rank"].data new_rank = form["rank"].data
if current_user.rank.atLeast(new_rank): if current_user.rank.at_least(new_rank):
if new_rank != user.rank: if new_rank != user.rank:
user.rank = form["rank"].data user.rank = form["rank"].data
msg = "Set rank of {} to {}".format(user.display_name, user.rank.get_title()) msg = "Set rank of {} to {}".format(user.display_name, user.rank.get_title())
addAuditLog(AuditSeverity.MODERATION, current_user, msg, add_audit_log(AuditSeverity.MODERATION, current_user, msg,
url_for("users.profile", username=username)) url_for("users.profile", username=username))
else: else:
flash(gettext("Can't promote a user to a rank higher than yourself!"), "danger") flash(gettext("Can't promote a user to a rank higher than yourself!"), "danger")
@ -356,8 +356,8 @@ def modtools_set_email(username):
user.email = request.form["email"] user.email = request.form["email"]
user.is_active = False user.is_active = False
token = randomString(32) token = random_string(32)
addAuditLog(AuditSeverity.MODERATION, current_user, f"Set email and sent a password reset on {user.username}", add_audit_log(AuditSeverity.MODERATION, current_user, f"Set email and sent a password reset on {user.username}",
url_for("users.profile", username=user.username), None) url_for("users.profile", username=user.username), None)
ver = UserEmailVerification() ver = UserEmailVerification()
@ -396,7 +396,7 @@ def modtools_ban(username):
else: else:
user.rank = UserRank.BANNED user.rank = UserRank.BANNED
addAuditLog(AuditSeverity.MODERATION, current_user, f"Banned {user.username}, expires {user.ban.expires_at or '-'}, message: {message}", add_audit_log(AuditSeverity.MODERATION, current_user, f"Banned {user.username}, expires {user.ban.expires_at or '-'}, message: {message}",
url_for("users.profile", username=user.username), None) url_for("users.profile", username=user.username), None)
db.session.commit() db.session.commit()
@ -420,7 +420,7 @@ def modtools_unban(username):
if user.rank == UserRank.BANNED: if user.rank == UserRank.BANNED:
user.rank = UserRank.MEMBER user.rank = UserRank.MEMBER
addAuditLog(AuditSeverity.MODERATION, current_user, f"Unbanned {user.username}", add_audit_log(AuditSeverity.MODERATION, current_user, f"Unbanned {user.username}",
url_for("users.profile", username=user.username), None) url_for("users.profile", username=user.username), None)
db.session.commit() db.session.commit()

@ -22,6 +22,7 @@ from sqlalchemy import and_, or_
from app.models import Package, PackageType, PackageState, PackageRelease from app.models import Package, PackageType, PackageState, PackageRelease
ValidationError = namedtuple("ValidationError", "status message") ValidationError = namedtuple("ValidationError", "status message")

@ -24,7 +24,7 @@ from flask_babel import lazy_gettext, LazyString
from app.logic.LogicError import LogicError from app.logic.LogicError import LogicError
from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \ from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \
License, UserRank, PackageDevState License, UserRank, PackageDevState
from app.utils import addAuditLog, has_blocked_domains, diff_dictionaries, describe_difference from app.utils import add_audit_log, has_blocked_domains, diff_dictionaries, describe_difference
from app.utils.url import clean_youtube_url from app.utils.url import clean_youtube_url
@ -173,7 +173,7 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
if not was_web and tag.is_protected: if not was_web and tag.is_protected:
continue continue
if tag.is_protected and tag not in old_tags and not user.rank.atLeast(UserRank.EDITOR): if tag.is_protected and tag not in old_tags and not user.rank.at_least(UserRank.EDITOR):
raise LogicError(400, lazy_gettext("Unable to add protected tag %(title)s to package", title=tag.title)) raise LogicError(400, lazy_gettext("Unable to add protected tag %(title)s to package", title=tag.title))
package.tags.append(tag) package.tags.append(tag)
@ -208,7 +208,7 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
msg += " [" + diff_desc + "]" msg += " [" + diff_desc + "]"
severity = AuditSeverity.NORMAL if user in package.maintainers else AuditSeverity.EDITOR severity = AuditSeverity.NORMAL if user in package.maintainers else AuditSeverity.EDITOR
addAuditLog(severity, user, msg, package.get_url("packages.view"), package, json.dumps(diff, indent=4)) add_audit_log(severity, user, msg, package.get_url("packages.view"), package, json.dumps(diff, indent=4))
db.session.commit() db.session.commit()

@ -23,8 +23,8 @@ from flask_babel import lazy_gettext
from app.logic.LogicError import LogicError from app.logic.LogicError import LogicError
from app.logic.uploads import upload_file from app.logic.uploads import upload_file
from app.models import PackageRelease, db, Permission, User, Package, MinetestRelease from app.models import PackageRelease, db, Permission, User, Package, MinetestRelease
from app.tasks.importtasks import makeVCSRelease, checkZipRelease from app.tasks.importtasks import make_vcs_release, check_zip_release
from app.utils import AuditSeverity, addAuditLog, nonEmptyOrNone from app.utils import AuditSeverity, add_audit_log, nonempty_or_none
def check_can_create_release(user: User, package: Package): def check_can_create_release(user: User, package: Package):
@ -54,11 +54,11 @@ def do_create_vcs_release(user: User, package: Package, title: str, ref: str,
msg = "Created release {}".format(rel.title) msg = "Created release {}".format(rel.title)
else: else:
msg = "Created release {} ({})".format(rel.title, reason) msg = "Created release {} ({})".format(rel.title, reason)
addAuditLog(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package) add_audit_log(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package)
db.session.commit() db.session.commit()
makeVCSRelease.apply_async((rel.id, nonEmptyOrNone(ref)), task_id=rel.task_id) make_vcs_release.apply_async((rel.id, nonempty_or_none(ref)), task_id=rel.task_id)
return rel return rel
@ -89,10 +89,10 @@ def do_create_zip_release(user: User, package: Package, title: str, file,
msg = "Created release {}".format(rel.title) msg = "Created release {}".format(rel.title)
else: else:
msg = "Created release {} ({})".format(rel.title, reason) msg = "Created release {} ({})".format(rel.title, reason)
addAuditLog(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package) add_audit_log(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package)
db.session.commit() db.session.commit()
checkZipRelease.apply_async((rel.id, uploaded_path), task_id=rel.task_id) check_zip_release.apply_async((rel.id, uploaded_path), task_id=rel.task_id)
return rel return rel

@ -21,7 +21,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 User, Package, PackageScreenshot, Permission, NotificationType, db, AuditSeverity from app.models import User, Package, PackageScreenshot, Permission, NotificationType, db, AuditSeverity
from app.utils import addNotification, addAuditLog from app.utils import add_notification, add_audit_log
from app.utils.image import get_image_size from app.utils.image import get_image_size
@ -58,8 +58,8 @@ def do_create_screenshot(user: User, package: Package, title: str, file, is_cove
else: else:
msg = "Created screenshot {} ({})".format(ss.title, reason) msg = "Created screenshot {} ({})".format(ss.title, reason)
addNotification(package.maintainers, user, NotificationType.PACKAGE_EDIT, msg, package.get_url("packages.view"), package) add_notification(package.maintainers, user, NotificationType.PACKAGE_EDIT, msg, package.get_url("packages.view"), package)
addAuditLog(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package) add_audit_log(AuditSeverity.NORMAL, user, msg, package.get_url("packages.view"), package)
db.session.commit() db.session.commit()

@ -21,7 +21,7 @@ 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
from app.utils import randomString from app.utils import random_string
def get_extension(filename): def get_extension(filename):
@ -59,7 +59,7 @@ def upload_file(file, file_type, file_type_desc):
file.stream.seek(0) file.stream.seek(0)
filename = randomString(10) + "." + 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)

@ -116,7 +116,7 @@ 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 user.rank.atLeast(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))
@ -181,7 +181,7 @@ class ForumTopic(db.Model):
raise Exception("Unknown permission given to ForumTopic.check_perm()") raise Exception("Unknown permission given to ForumTopic.check_perm()")
if perm == Permission.TOPIC_DISCARD: if perm == Permission.TOPIC_DISCARD:
return self.author == user or user.rank.atLeast(UserRank.EDITOR) return self.author == user or user.rank.at_least(UserRank.EDITOR)
else: else:
raise Exception("Permission {} is not related to topics".format(perm.name)) raise Exception("Permission {} is not related to topics".format(perm.name))

@ -271,7 +271,7 @@ class Dependency(db.Model):
else: else:
raise Exception("Either meta or package must be given, but not both!") raise Exception("Either meta or package must be given, but not both!")
def getName(self): def get_name(self):
if self.meta_package: if self.meta_package:
return self.meta_package.name return self.meta_package.name
elif self.package: elif self.package:
@ -484,7 +484,7 @@ class Package(db.Model):
query = query.filter_by(optional=not is_hard) query = query.filter_by(optional=not is_hard)
deps = query.all() deps = query.all()
deps.sort(key=lambda x: x.getName()) deps.sort(key=lambda x: x.get_name())
return deps return deps
def get_sorted_hard_dependencies(self): def get_sorted_hard_dependencies(self):
@ -654,25 +654,25 @@ class Package(db.Model):
raise Exception("Unknown permission given to Package.check_perm()") raise Exception("Unknown permission given to Package.check_perm()")
is_owner = user == self.author is_owner = user == self.author
is_maintainer = is_owner or user.rank.atLeast(UserRank.EDITOR) or user in self.maintainers is_maintainer = is_owner or user.rank.at_least(UserRank.EDITOR) or user in self.maintainers
is_approver = user.rank.atLeast(UserRank.APPROVER) is_approver = user.rank.at_least(UserRank.APPROVER)
if perm == Permission.CREATE_THREAD: if perm == Permission.CREATE_THREAD:
return user.rank.atLeast(UserRank.NEW_MEMBER) return user.rank.at_least(UserRank.NEW_MEMBER)
# Members can edit their own packages, and editors can edit any packages # Members can edit their own packages, and editors can edit any packages
elif perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS: elif perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS:
return is_maintainer return is_maintainer
elif perm == Permission.EDIT_PACKAGE: elif perm == Permission.EDIT_PACKAGE:
return is_maintainer and user.rank.atLeast(UserRank.NEW_MEMBER) return is_maintainer and user.rank.at_least(UserRank.NEW_MEMBER)
elif perm == Permission.APPROVE_RELEASE: elif perm == Permission.APPROVE_RELEASE:
return (is_maintainer or is_approver) and user.rank.atLeast(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER) return (is_maintainer or is_approver) and user.rank.at_least(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)
# Anyone can change the package name when not approved, but only editors when approved # Anyone can change the package name when not approved, but only editors when approved
elif perm == Permission.CHANGE_NAME: elif perm == Permission.CHANGE_NAME:
return not self.approved or user.rank.atLeast(UserRank.EDITOR) return not self.approved or user.rank.at_least(UserRank.EDITOR)
# Editors can change authors and approve new packages # Editors can change authors and approve new packages
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR: elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
@ -680,16 +680,16 @@ class Package(db.Model):
elif perm == Permission.APPROVE_SCREENSHOT: elif perm == Permission.APPROVE_SCREENSHOT:
return (is_maintainer or is_approver) and \ return (is_maintainer or is_approver) and \
user.rank.atLeast(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER) user.rank.at_least(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)
elif perm == Permission.EDIT_MAINTAINERS or perm == Permission.DELETE_PACKAGE: elif perm == Permission.EDIT_MAINTAINERS or perm == Permission.DELETE_PACKAGE:
return is_owner or user.rank.atLeast(UserRank.EDITOR) return is_owner or user.rank.at_least(UserRank.EDITOR)
elif perm == Permission.UNAPPROVE_PACKAGE: elif perm == Permission.UNAPPROVE_PACKAGE:
return is_owner or user.rank.atLeast(UserRank.APPROVER) return is_owner or user.rank.at_least(UserRank.APPROVER)
elif perm == Permission.CHANGE_RELEASE_URL: elif perm == Permission.CHANGE_RELEASE_URL:
return user.rank.atLeast(UserRank.MODERATOR) return user.rank.at_least(UserRank.MODERATOR)
else: else:
raise Exception("Permission {} is not related to packages".format(perm.name)) raise Exception("Permission {} is not related to packages".format(perm.name))
@ -738,7 +738,7 @@ class Package(db.Model):
elif state == PackageState.WIP: elif state == PackageState.WIP:
return self.check_perm(user, Permission.EDIT_PACKAGE) and \ return self.check_perm(user, Permission.EDIT_PACKAGE) and \
(user in self.maintainers or user.rank.atLeast(UserRank.ADMIN)) (user in self.maintainers or user.rank.at_least(UserRank.ADMIN))
return True return True
@ -1018,10 +1018,10 @@ class PackageRelease(db.Model):
is_maintainer = user == self.package.author or user in self.package.maintainers is_maintainer = user == self.package.author or user in self.package.maintainers
if perm == Permission.DELETE_RELEASE: if perm == Permission.DELETE_RELEASE:
if user.rank.atLeast(UserRank.ADMIN): if user.rank.at_least(UserRank.ADMIN):
return True return True
if not (is_maintainer or user.rank.atLeast(UserRank.EDITOR)): if not (is_maintainer or user.rank.at_least(UserRank.EDITOR)):
return False return False
if not self.package.approved or self.task_id is not None: if not self.package.approved or self.task_id is not None:
@ -1033,8 +1033,8 @@ class PackageRelease(db.Model):
return count > 0 return count > 0
elif perm == Permission.APPROVE_RELEASE: elif perm == Permission.APPROVE_RELEASE:
return user.rank.atLeast(UserRank.APPROVER) or \ return user.rank.at_least(UserRank.APPROVER) or \
(is_maintainer and user.rank.atLeast( (is_maintainer and user.rank.at_least(
UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)) UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER))
else: else:
raise Exception("Permission {} is not related to releases".format(perm.name)) raise Exception("Permission {} is not related to releases".format(perm.name))

@ -92,21 +92,21 @@ class Thread(db.Model):
if self.package: if self.package:
isMaintainer = isMaintainer or user in self.package.maintainers isMaintainer = isMaintainer or user in self.package.maintainers
canSee = not self.private or isMaintainer or user.rank.atLeast(UserRank.APPROVER) or user in self.watchers canSee = not self.private or isMaintainer or user.rank.at_least(UserRank.APPROVER) or user in self.watchers
if perm == Permission.SEE_THREAD: if perm == Permission.SEE_THREAD:
return canSee return canSee
elif perm == Permission.COMMENT_THREAD: elif perm == Permission.COMMENT_THREAD:
return canSee and (not self.locked or user.rank.atLeast(UserRank.MODERATOR)) return canSee and (not self.locked or user.rank.at_least(UserRank.MODERATOR))
elif perm == Permission.LOCK_THREAD: elif perm == Permission.LOCK_THREAD:
return user.rank.atLeast(UserRank.MODERATOR) return user.rank.at_least(UserRank.MODERATOR)
elif perm == Permission.DELETE_THREAD: elif perm == Permission.DELETE_THREAD:
from app.utils.models import get_system_user from app.utils.models import get_system_user
return (self.author == get_system_user() and self.package and return (self.author == get_system_user() and self.package and
user in self.package.maintainers) or user.rank.atLeast(UserRank.MODERATOR) user in self.package.maintainers) or user.rank.at_least(UserRank.MODERATOR)
else: else:
raise Exception("Permission {} is not related to threads".format(perm.name)) raise Exception("Permission {} is not related to threads".format(perm.name))
@ -157,10 +157,10 @@ class ThreadReply(db.Model):
raise Exception("Unknown permission given to ThreadReply.check_perm()") raise Exception("Unknown permission given to ThreadReply.check_perm()")
if perm == Permission.EDIT_REPLY: if perm == Permission.EDIT_REPLY:
return user.rank.atLeast(UserRank.NEW_MEMBER if user == self.author else UserRank.MODERATOR) and not self.thread.locked return user.rank.at_least(UserRank.NEW_MEMBER if user == self.author else UserRank.MODERATOR) and not self.thread.locked
elif perm == Permission.DELETE_REPLY: elif perm == Permission.DELETE_REPLY:
return user.rank.atLeast(UserRank.MODERATOR) and self.thread.first_reply != self return user.rank.at_least(UserRank.MODERATOR) and self.thread.first_reply != self
else: else:
raise Exception("Permission {} is not related to threads".format(perm.name)) raise Exception("Permission {} is not related to threads".format(perm.name))
@ -227,7 +227,7 @@ class PackageReview(db.Model):
name=self.package.name, name=self.package.name,
reviewer=self.author.username) reviewer=self.author.username)
def getVoteUrl(self, next_url=None): def get_vote_url(self, next_url=None):
return url_for("packages.review_vote", return url_for("packages.review_vote",
author=self.package.author.username, author=self.package.author.username,
name=self.package.name, name=self.package.name,
@ -248,7 +248,7 @@ class PackageReview(db.Model):
raise Exception("Unknown permission given to PackageReview.check_perm()") raise Exception("Unknown permission given to PackageReview.check_perm()")
if perm == Permission.DELETE_REVIEW: if perm == Permission.DELETE_REVIEW:
return user == self.author or user.rank.atLeast(UserRank.MODERATOR) return user == self.author or user.rank.at_least(UserRank.MODERATOR)
else: else:
raise Exception("Permission {} is not related to reviews".format(perm.name)) raise Exception("Permission {} is not related to reviews".format(perm.name))

@ -37,7 +37,7 @@ class UserRank(enum.Enum):
MODERATOR = 8 MODERATOR = 8
ADMIN = 9 ADMIN = 9
def atLeast(self, min): def at_least(self, min):
return self.value >= min.value return self.value >= min.value
def get_title(self): def get_title(self):
@ -101,10 +101,10 @@ class Permission(enum.Enum):
self == Permission.APPROVE_RELEASE or \ self == Permission.APPROVE_RELEASE or \
self == Permission.APPROVE_SCREENSHOT or \ self == Permission.APPROVE_SCREENSHOT or \
self == Permission.SEE_THREAD: self == Permission.SEE_THREAD:
return user.rank.atLeast(UserRank.APPROVER) return user.rank.at_least(UserRank.APPROVER)
elif self == Permission.EDIT_TAGS or self == Permission.CREATE_TAG: elif self == Permission.EDIT_TAGS or self == Permission.CREATE_TAG:
return user.rank.atLeast(UserRank.EDITOR) return user.rank.at_least(UserRank.EDITOR)
else: else:
raise Exception("Non-global permission checked globally. Use Package.check_perm or User.check_perm instead.") raise Exception("Non-global permission checked globally. Use Package.check_perm or User.check_perm instead.")
@ -234,20 +234,20 @@ class User(db.Model, UserMixin):
# Members can edit their own packages, and editors can edit any packages # Members can edit their own packages, and editors can edit any packages
if perm == Permission.CHANGE_AUTHOR: if perm == Permission.CHANGE_AUTHOR:
return user.rank.atLeast(UserRank.EDITOR) return user.rank.at_least(UserRank.EDITOR)
elif perm == Permission.CHANGE_USERNAMES: elif perm == Permission.CHANGE_USERNAMES:
return user.rank.atLeast(UserRank.MODERATOR) return user.rank.at_least(UserRank.MODERATOR)
elif perm == Permission.CHANGE_RANK: elif perm == Permission.CHANGE_RANK:
return user.rank.atLeast(UserRank.MODERATOR) and not self.rank.atLeast(user.rank) return user.rank.at_least(UserRank.MODERATOR) and not self.rank.at_least(user.rank)
elif perm == Permission.CHANGE_EMAIL or perm == Permission.CHANGE_PROFILE_URLS: elif perm == Permission.CHANGE_EMAIL or perm == Permission.CHANGE_PROFILE_URLS:
return user == self or (user.rank.atLeast(UserRank.MODERATOR) and not self.rank.atLeast(user.rank)) return user == self or (user.rank.at_least(UserRank.MODERATOR) and not self.rank.at_least(user.rank))
elif perm == Permission.CHANGE_DISPLAY_NAME: elif perm == Permission.CHANGE_DISPLAY_NAME:
return user.rank.atLeast(UserRank.NEW_MEMBER if user == self else UserRank.MODERATOR) return user.rank.at_least(UserRank.NEW_MEMBER if user == self else UserRank.MODERATOR)
elif perm == Permission.CREATE_TOKEN: elif perm == Permission.CREATE_TOKEN:
if user == self: if user == self:
return user.rank.atLeast(UserRank.NEW_MEMBER) return user.rank.at_least(UserRank.NEW_MEMBER)
else: else:
return user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank) return user.rank.at_least(UserRank.MODERATOR) and user.rank.at_least(self.rank)
else: else:
raise Exception("Permission {} is not related to users".format(perm.name)) raise Exception("Permission {} is not related to users".format(perm.name))
@ -255,11 +255,11 @@ class User(db.Model, UserMixin):
from app.models import ThreadReply from app.models import ThreadReply
factor = 1 factor = 1
if self.rank.atLeast(UserRank.ADMIN): if self.rank.at_least(UserRank.ADMIN):
return True return True
elif self.rank.atLeast(UserRank.TRUSTED_MEMBER): elif self.rank.at_least(UserRank.TRUSTED_MEMBER):
factor = 3 factor = 3
elif self.rank.atLeast(UserRank.MEMBER): elif self.rank.at_least(UserRank.MEMBER):
factor = 2 factor = 2
one_min_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=1) one_min_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=1)
@ -278,11 +278,11 @@ class User(db.Model, UserMixin):
from app.models import Thread from app.models import Thread
factor = 1 factor = 1
if self.rank.atLeast(UserRank.ADMIN): if self.rank.at_least(UserRank.ADMIN):
return True return True
elif self.rank.atLeast(UserRank.TRUSTED_MEMBER): elif self.rank.at_least(UserRank.TRUSTED_MEMBER):
factor = 5 factor = 5
elif self.rank.atLeast(UserRank.MEMBER): elif self.rank.at_least(UserRank.MEMBER):
factor = 2 factor = 2
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1) hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
@ -293,9 +293,9 @@ class User(db.Model, UserMixin):
from app.models import PackageReview from app.models import PackageReview
factor = 1 factor = 1
if self.rank.atLeast(UserRank.ADMIN): if self.rank.at_least(UserRank.ADMIN):
return True return True
elif self.rank.atLeast(UserRank.TRUSTED_MEMBER): elif self.rank.at_least(UserRank.TRUSTED_MEMBER):
factor *= 5 factor *= 5
five_mins_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=5) five_mins_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=5)

@ -23,7 +23,7 @@ from sqlalchemy_searchable import search
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag, \ from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag, \
ContentWarning, PackageState, PackageDevState ContentWarning, PackageState, PackageDevState
from .utils import isYes, get_int_or_abort from .utils import is_yes, get_int_or_abort
class QueryBuilder: class QueryBuilder:
@ -105,10 +105,10 @@ class QueryBuilder:
else: else:
self.version = None self.version = None
self.show_discarded = isYes(args.get("show_discarded")) self.show_discarded = is_yes(args.get("show_discarded"))
self.show_added = args.get("show_added") self.show_added = args.get("show_added")
if self.show_added is not None: if self.show_added is not None:
self.show_added = isYes(self.show_added) self.show_added = is_yes(self.show_added)
if self.search is not None and self.search.strip() == "": if self.search is not None and self.search.strip() == "":
self.search = None self.search = None
@ -117,12 +117,12 @@ class QueryBuilder:
if self.game: if self.game:
self.game = Package.get_by_key(self.game) self.game = Package.get_by_key(self.game)
def setSortIfNone(self, name, dir="desc"): def set_sort_if_none(self, name, dir="desc"):
if self.order_by is None: if self.order_by is None:
self.order_by = name self.order_by = name
self.order_dir = dir self.order_dir = dir
def getReleases(self): def get_releases(self):
releases_query = db.session.query(PackageRelease.package_id, func.max(PackageRelease.id)) \ releases_query = db.session.query(PackageRelease.package_id, func.max(PackageRelease.id)) \
.select_from(PackageRelease).filter(PackageRelease.approved) \ .select_from(PackageRelease).filter(PackageRelease.approved) \
.group_by(PackageRelease.package_id) .group_by(PackageRelease.package_id)
@ -136,18 +136,18 @@ class QueryBuilder:
return releases_query.all() return releases_query.all()
def convertToDictionary(self, packages): def convert_to_dictionary(self, packages):
releases = {} releases = {}
for [package_id, release_id] in self.getReleases(): for [package_id, release_id] in self.get_releases():
releases[package_id] = release_id releases[package_id] = release_id
def toJson(package: Package): def to_json(package: Package):
release_id = releases.get(package.id) release_id = releases.get(package.id)
return package.as_short_dict(current_app.config["BASE_URL"], release_id=release_id, no_load=True) return package.as_short_dict(current_app.config["BASE_URL"], release_id=release_id, no_load=True)
return [toJson(pkg) for pkg in packages] return [to_json(pkg) for pkg in packages]
def buildPackageQuery(self): def build_package_query(self):
if self.order_by == "last_release": if self.order_by == "last_release":
query = db.session.query(Package).select_from(PackageRelease).join(Package) \ query = db.session.query(Package).select_from(PackageRelease).join(Package) \
.filter_by(state=PackageState.APPROVED) .filter_by(state=PackageState.APPROVED)
@ -156,14 +156,14 @@ class QueryBuilder:
query = query.options(subqueryload(Package.main_screenshot), subqueryload(Package.aliases)) query = query.options(subqueryload(Package.main_screenshot), subqueryload(Package.aliases))
query = self.orderPackageQuery(self.filterPackageQuery(query)) query = self.order_package_query(self.filter_package_query(query))
if self.limit: if self.limit:
query = query.limit(self.limit) query = query.limit(self.limit)
return query return query
def filterPackageQuery(self, query): def filter_package_query(self, query):
if len(self.types) > 0: if len(self.types) > 0:
query = query.filter(Package.type.in_(self.types)) query = query.filter(Package.type.in_(self.types))
@ -207,7 +207,7 @@ class QueryBuilder:
return query return query
def orderPackageQuery(self, query): def order_package_query(self, query):
if self.search: if self.search:
query = search(query, self.search, sort=self.order_by is None) query = search(query, self.search, sort=self.order_by is None)
@ -250,7 +250,7 @@ class QueryBuilder:
return query return query
def buildTopicQuery(self, show_added=False): def build_topic_query(self, show_added=False):
query = ForumTopic.query query = ForumTopic.query
if not self.show_discarded: if not self.show_discarded:

@ -75,11 +75,11 @@ celery = make_celery(app)
CELERYBEAT_SCHEDULE = { CELERYBEAT_SCHEDULE = {
'topic_list_import': { 'topic_list_import': {
'task': 'app.tasks.forumtasks.importTopicList', 'task': 'app.tasks.forumtasks.import_topic_list',
'schedule': crontab(minute=1, hour=1), # 0101 'schedule': crontab(minute=1, hour=1), # 0101
}, },
'package_score_update': { 'package_score_update': {
'task': 'app.tasks.pkgtasks.updatePackageScores', 'task': 'app.tasks.pkgtasks.update_package_scores',
'schedule': crontab(minute=10, hour=1), # 0110 'schedule': crontab(minute=10, hour=1), # 0110
}, },
'check_for_updates': { 'check_for_updates': {

@ -23,7 +23,7 @@ from flask_mail import Message
from app import mail from app import mail
from app.models import Notification, db, EmailSubscription, User from app.models import Notification, db, EmailSubscription, User
from app.tasks import celery from app.tasks import celery
from app.utils import abs_url_for, abs_url, randomString from app.utils import abs_url_for, abs_url, random_string
def get_email_subscription(email): def get_email_subscription(email):
@ -31,7 +31,7 @@ def get_email_subscription(email):
ret = EmailSubscription.query.filter_by(email=email).first() ret = EmailSubscription.query.filter_by(email=email).first()
if not ret: if not ret:
ret = EmailSubscription(email) ret = EmailSubscription(email)
ret.token = randomString(32) ret.token = random_string(32)
db.session.add(ret) db.session.add(ret)
db.session.commit() db.session.commit()

@ -23,15 +23,15 @@ from urllib.parse import urljoin
from app.models import User, db, PackageType, ForumTopic from app.models import User, db, PackageType, ForumTopic
from app.tasks import celery from app.tasks import celery
from app.utils import is_username_valid from app.utils import is_username_valid
from app.utils.phpbbparser import getProfile, getTopicsFromForum from app.utils.phpbbparser import get_profile, get_topics_from_forum
from .usertasks import set_profile_picture_from_url from .usertasks import set_profile_picture_from_url
@celery.task() @celery.task()
def checkForumAccount(forums_username): def check_forum_account(forums_username):
print("### Checking " + forums_username, file=sys.stderr) print("### Checking " + forums_username, file=sys.stderr)
try: try:
profile = getProfile("https://forum.minetest.net", forums_username) profile = get_profile("https://forum.minetest.net", forums_username)
except OSError as e: except OSError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
return return
@ -42,7 +42,7 @@ def checkForumAccount(forums_username):
user = User.query.filter_by(forums_username=forums_username).first() user = User.query.filter_by(forums_username=forums_username).first()
# Create user # Create user
needsSaving = False needs_saving = False
if user is None: if user is None:
user = User(forums_username) user = User(forums_username)
user.forums_username = forums_username user.forums_username = forums_username
@ -53,14 +53,14 @@ def checkForumAccount(forums_username):
if github_username is not None and github_username.strip() != "": if github_username is not None and github_username.strip() != "":
print("Updated GitHub username for " + user.display_name + " to " + github_username) print("Updated GitHub username for " + user.display_name + " to " + github_username)
user.github_username = github_username user.github_username = github_username
needsSaving = True needs_saving = True
pic = profile.avatar pic = profile.avatar
if pic and pic.startswith("http"): if pic and pic.startswith("http"):
pic = None pic = None
# Save # Save
if needsSaving: if needs_saving:
db.session.commit() db.session.commit()
if pic: if pic:
@ -74,21 +74,21 @@ def checkForumAccount(forums_username):
print(f"####### Queueing", file=sys.stderr) print(f"####### Queueing", file=sys.stderr)
set_profile_picture_from_url.delay(user.username, pic) set_profile_picture_from_url.delay(user.username, pic)
return needsSaving return needs_saving
@celery.task() @celery.task()
def checkAllForumAccounts(): def check_all_forum_accounts():
query = User.query.filter(User.forums_username.isnot(None)) query = User.query.filter(User.forums_username.isnot(None))
for user in query.all(): for user in query.all():
checkForumAccount(user.forums_username) check_forum_account(user.forums_username)
regex_tag = re.compile(r"\[([a-z0-9_]+)\]") regex_tag = re.compile(r"\[([a-z0-9_]+)\]")
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api", "beta", "alpha", "git"] BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api", "beta", "alpha", "git"]
def getNameFromTaglist(taglist): def get_name_from_taglist(taglist):
for tag in reversed(regex_tag.findall(taglist)): for tag in reversed(regex_tag.findall(taglist)):
if len(tag) < 30 and not tag in BANNED_NAMES and \ if len(tag) < 30 and not tag in BANNED_NAMES and \
not re.match(r"^[a-z]?[0-9]+$", tag): not re.match(r"^[a-z]?[0-9]+$", tag):
@ -100,15 +100,16 @@ def getNameFromTaglist(taglist):
regex_title = re.compile(r"^((?:\[[^\]]+\] *)*)([^\[]+) *((?:\[[^\]]+\] *)*)[^\[]*$") regex_title = re.compile(r"^((?:\[[^\]]+\] *)*)([^\[]+) *((?:\[[^\]]+\] *)*)[^\[]*$")
def parseTitle(title): def parse_title(title):
m = regex_title.match(title) m = regex_title.match(title)
if m is None: if m is None:
print("Invalid title format: " + title) print("Invalid title format: " + title)
return title, getNameFromTaglist(title) return title, get_name_from_taglist(title)
else: else:
return m.group(2).strip(), getNameFromTaglist(m.group(3)) return m.group(2).strip(), get_name_from_taglist(m.group(3))
def getLinksFromModSearch():
def get_links_from_mod_search():
links = {} links = {}
try: try:
@ -127,15 +128,16 @@ def getLinksFromModSearch():
return links return links
@celery.task() @celery.task()
def importTopicList(): def import_topic_list():
links_by_id = getLinksFromModSearch() links_by_id = get_links_from_mod_search()
info_by_id = {} info_by_id = {}
getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': False }) get_topics_from_forum(11, out=info_by_id, extra={'type': PackageType.MOD, 'wip': False})
getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': True }) get_topics_from_forum(9, out=info_by_id, extra={'type': PackageType.MOD, 'wip': True})
getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False }) get_topics_from_forum(15, out=info_by_id, extra={'type': PackageType.GAME, 'wip': False})
getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True }) get_topics_from_forum(50, out=info_by_id, extra={'type': PackageType.GAME, 'wip': True})
# Caches # Caches
username_to_user = {} username_to_user = {}
@ -182,7 +184,7 @@ def importTopicList():
db.session.add(topic) db.session.add(topic)
# Parse title # Parse title
title, name = parseTitle(info["title"]) title, name = parse_title(info["title"])
# Get link # Get link
link = links_by_id.get(id) link = links_by_id.get(id)

@ -30,7 +30,7 @@ from kombu import uuid
from app.models import AuditSeverity, db, NotificationType, PackageRelease, MetaPackage, Dependency, PackageType, \ from app.models import AuditSeverity, db, NotificationType, PackageRelease, MetaPackage, Dependency, PackageType, \
MinetestRelease, Package, PackageState, PackageScreenshot, PackageUpdateTrigger, PackageUpdateConfig MinetestRelease, Package, PackageState, PackageScreenshot, PackageUpdateTrigger, PackageUpdateConfig
from app.tasks import celery, TaskError from app.tasks import celery, TaskError
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog, get_games_from_csv from app.utils import random_string, post_bot_message, add_system_notification, add_system_audit_log, get_games_from_csv
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir
from .minetestcheck import build_tree, MinetestCheckError, ContentType from .minetestcheck import build_tree, MinetestCheckError, ContentType
from app import app from app import app
@ -41,7 +41,7 @@ from app.utils.image import get_image_size
@celery.task() @celery.task()
def getMeta(urlstr, author): 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)
@ -82,13 +82,13 @@ def getMeta(urlstr, author):
@celery.task() @celery.task()
def updateAllGameSupport(): def update_all_game_support():
resolver = GameSupportResolver(db.session) resolver = GameSupportResolver(db.session)
resolver.init_all() resolver.init_all()
db.session.commit() db.session.commit()
def postReleaseCheckUpdate(self, release: PackageRelease, path): def post_release_check_update(self, release: PackageRelease, path):
try: try:
tree = build_tree(path, expected_type=ContentType[release.package.type.name], tree = build_tree(path, expected_type=ContentType[release.package.type.name],
author=release.package.author.username, name=release.package.name) author=release.package.author.username, name=release.package.name)
@ -97,14 +97,14 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
raise MinetestCheckError(f"Expected {tree.relative} to have technical name {release.package.name}, instead has name {tree.name}") raise MinetestCheckError(f"Expected {tree.relative} to have technical name {release.package.name}, instead has name {tree.name}")
cache = {} cache = {}
def getMetaPackages(names): def get_meta_packages(names):
return [ MetaPackage.GetOrCreate(x, cache) for x in names ] return [ MetaPackage.GetOrCreate(x, cache) for x in names ]
provides = tree.get_mod_names() provides = tree.get_mod_names()
package = release.package package = release.package
package.provides.clear() package.provides.clear()
package.provides.extend(getMetaPackages(tree.get_mod_names())) package.provides.extend(get_meta_packages(tree.get_mod_names()))
# Delete all mod name dependencies # Delete all mod name dependencies
package.dependencies.filter(Dependency.meta_package != None).delete() package.dependencies.filter(Dependency.meta_package != None).delete()
@ -124,10 +124,10 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
raise MinetestCheckError("Game has unresolved hard dependencies: " + deps) raise MinetestCheckError("Game has unresolved hard dependencies: " + deps)
# Add dependencies # Add dependencies
for meta in getMetaPackages(depends): for meta in get_meta_packages(depends):
db.session.add(Dependency(package, meta=meta, optional=False)) db.session.add(Dependency(package, meta=meta, optional=False))
for meta in getMetaPackages(optional_depends): for meta in get_meta_packages(optional_depends):
db.session.add(Dependency(package, meta=meta, optional=True)) db.session.add(Dependency(package, meta=meta, optional=True))
# Update min/max # Update min/max
@ -191,7 +191,7 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
@celery.task(bind=True) @celery.task(bind=True)
def checkZipRelease(self, id, path): def check_zip_release(self, id, path):
release = PackageRelease.query.get(id) release = PackageRelease.query.get(id)
if release is None: if release is None:
raise TaskError("No such release!") raise TaskError("No such release!")
@ -202,7 +202,7 @@ def checkZipRelease(self, id, path):
with ZipFile(path, 'r') as zip_ref: with ZipFile(path, 'r') as zip_ref:
zip_ref.extractall(temp) zip_ref.extractall(temp)
postReleaseCheckUpdate(self, release, temp) post_release_check_update(self, release, temp)
release.task_id = None release.task_id = None
release.approve(release.package.author) release.approve(release.package.author)
@ -210,7 +210,7 @@ def checkZipRelease(self, id, path):
@celery.task(bind=True) @celery.task(bind=True)
def makeVCSRelease(self, id, branch): def make_vcs_release(self, id, branch):
release = PackageRelease.query.get(id) release = PackageRelease.query.get(id)
if release is None: if release is None:
raise TaskError("No such release!") raise TaskError("No such release!")
@ -218,9 +218,9 @@ def makeVCSRelease(self, id, branch):
raise TaskError("No package attached to release") raise TaskError("No package attached to release")
with clone_repo(release.package.repo, ref=branch, recursive=True) as repo: with clone_repo(release.package.repo, ref=branch, recursive=True) as repo:
postReleaseCheckUpdate(self, release, repo.working_tree_dir) post_release_check_update(self, release, repo.working_tree_dir)
filename = randomString(10) + ".zip" filename = random_string(10) + ".zip"
destPath = os.path.join(app.config["UPLOAD_DIR"], filename) destPath = os.path.join(app.config["UPLOAD_DIR"], filename)
assert(not os.path.isfile(destPath)) assert(not os.path.isfile(destPath))
@ -238,7 +238,7 @@ def makeVCSRelease(self, id, branch):
@celery.task() @celery.task()
def importRepoScreenshot(id): def import_repo_screenshot(id):
package = Package.query.get(id) package = Package.query.get(id)
if package is None or package.state == PackageState.DELETED: if package is None or package.state == PackageState.DELETED:
raise Exception("Unexpected none package") raise Exception("Unexpected none package")
@ -248,7 +248,7 @@ def importRepoScreenshot(id):
for ext in ["png", "jpg", "jpeg"]: for ext in ["png", "jpg", "jpeg"]:
sourcePath = repo.working_tree_dir + "/screenshot." + ext sourcePath = repo.working_tree_dir + "/screenshot." + ext
if os.path.isfile(sourcePath): if os.path.isfile(sourcePath):
filename = randomString(10) + "." + ext filename = random_string(10) + "." + ext
destPath = os.path.join(app.config["UPLOAD_DIR"], filename) destPath = os.path.join(app.config["UPLOAD_DIR"], filename)
shutil.copyfile(sourcePath, destPath) shutil.copyfile(sourcePath, destPath)
@ -313,11 +313,11 @@ def check_update_config_impl(package):
db.session.add(rel) db.session.add(rel)
msg = "Created release {} (Git Update Detection)".format(rel.title) msg = "Created release {} (Git Update Detection)".format(rel.title)
addSystemAuditLog(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() db.session.commit()
makeVCSRelease.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:
config.set_outdated() config.set_outdated()
@ -338,7 +338,7 @@ def check_update_config_impl(package):
.format(tag, msg_last) .format(tag, msg_last)
for user in package.maintainers: for user in package.maintainers:
addSystemNotification(user, NotificationType.BOT, add_system_notification(user, NotificationType.BOT,
msg, url_for("todo.view_user", username=user.username, _external=False), package) msg, url_for("todo.view_user", username=user.username, _external=False), package)
config.last_commit = commit config.last_commit = commit

@ -19,7 +19,7 @@ from app.models import Package, db
from app.tasks import celery from app.tasks import celery
@celery.task() @celery.task()
def updatePackageScores(): def update_package_scores():
Package.query.update({ "score_downloads": Package.score_downloads * 0.95 }) Package.query.update({ "score_downloads": Package.score_downloads * 0.95 })
db.session.commit() db.session.commit()

@ -23,7 +23,7 @@ from sqlalchemy import or_, and_
from app import app from app import app
from app.models import User, db, UserRank, ThreadReply, Package from app.models import User, db, UserRank, ThreadReply, Package
from app.utils import randomString from app.utils import random_string
from app.utils.models import create_session from app.utils.models import create_session
from app.tasks import celery, TaskError from app.tasks import celery, TaskError
@ -76,7 +76,7 @@ def set_profile_picture_from_url(username: str, url: str):
else: else:
raise TaskError(f"Unacceptable content-type: {content_type}") raise TaskError(f"Unacceptable content-type: {content_type}")
filename = randomString(10) + "." + ext filename = random_string(10) + "." + ext
filepath = os.path.join(app.config["UPLOAD_DIR"], filename) filepath = os.path.join(app.config["UPLOAD_DIR"], filename)
with open(filepath, "wb") as f: with open(filepath, "wb") as f:
size = 0 size = 0

@ -147,10 +147,10 @@
{{ _("Statistics") }} {{ _("Statistics") }}
</a> </a>
</li> </li>
{% if current_user.rank.atLeast(current_user.rank.EDITOR) or check_global_perm(current_user, "CREATE_TAG") %} {% if current_user.rank.at_least(current_user.rank.EDITOR) or check_global_perm(current_user, "CREATE_TAG") %}
<li class="dropdown-divider"></li> <li class="dropdown-divider"></li>
{% endif %} {% endif %}
{% if current_user.rank.atLeast(current_user.rank.MODERATOR) %} {% if current_user.rank.at_least(current_user.rank.MODERATOR) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('admin.audit') }}"> <a class="nav-link" href="{{ url_for('admin.audit') }}">
{{ _("Audit Log") }} {{ _("Audit Log") }}
@ -164,7 +164,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if current_user.rank.atLeast(current_user.rank.EDITOR) %} {% if current_user.rank.at_least(current_user.rank.EDITOR) %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.restore') }}">{{ _("Restore Package") }}</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('admin.restore') }}">{{ _("Restore Package") }}</a></li>
{% endif %} {% endif %}
{% if check_global_perm(current_user, "EDIT_TAGS") %} {% if check_global_perm(current_user, "EDIT_TAGS") %}

@ -1,6 +1,6 @@
{% macro render_review_vote(review, current_user, next_url) %} {% macro render_review_vote(review, current_user, next_url) %}
{% set (positive, negative, is_positive) = review.get_totals(current_user) %} {% set (positive, negative, is_positive) = review.get_totals(current_user) %}
<form class="-group" method="post" action="{{ review.getVoteUrl(next_url) }}"> <form class="-group" method="post" action="{{ review.get_vote_url(next_url) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="btn-group"> <div class="btn-group">
<button class="btn {% if is_positive == true %}btn-primary{% else %}btn-secondary{% endif %}" name="is_positive" value="yes"> <button class="btn {% if is_positive == true %}btn-primary{% else %}btn-secondary{% endif %}" name="is_positive" value="yes">

@ -25,7 +25,7 @@
<td class="btn-group"> <td class="btn-group">
{% if current_user == topic.author or topic.author.check_perm(current_user, "CHANGE_AUTHOR") %} {% if current_user == topic.author or topic.author.check_perm(current_user, "CHANGE_AUTHOR") %}
<a class="btn btn-primary" <a class="btn btn-primary"
href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}"> href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.get_repo_url(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">
{{ _("Create") }} {{ _("Create") }}
</a> </a>
{% endif %} {% endif %}
@ -61,7 +61,7 @@
{% endif %} {% endif %}
{% if topic.author == current_user or topic.author.check_perm(current_user, "CHANGE_AUTHOR") %} {% if topic.author == current_user or topic.author.check_perm(current_user, "CHANGE_AUTHOR") %}
| |
<a href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}"> <a href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.get_repo_url(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">
{{ _("Create") }} {{ _("Create") }}
</a> </a>
{% endif %} {% endif %}

@ -323,7 +323,7 @@
</p> </p>
{% endif %} {% endif %}
{% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.ADMIN) %} {% if current_user.is_authenticated and current_user.rank.at_least(current_user.rank.ADMIN) %}
<a href="{{ package.get_url('packages.review_votes') }}" class="btn btn-secondary">{{ _("Review Votes") }}</a> <a href="{{ package.get_url('packages.review_votes') }}" class="btn btn-secondary">{{ _("Review Votes") }}</a>
{% endif %} {% endif %}

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<h2 class="mb-4">{{ _("Approval Queue") }}</h2> <h2 class="mb-4">{{ _("Approval Queue") }}</h2>
{% if canApproveScn and screenshots %} {% if can_approve_scn and screenshots %}
<div class="card my-4"> <div class="card my-4">
<h3 class="card-header">{{ _("Screenshots") }} <h3 class="card-header">{{ _("Screenshots") }}
<form class="float-right" method="post" action="{{ url_for('todo.view_editor') }}"> <form class="float-right" method="post" action="{{ url_for('todo.view_editor') }}">
@ -40,7 +40,7 @@
{% endif %} {% endif %}
<div class="row"> <div class="row">
{% if canApproveNew and (packages or wip_packages) %} {% if can_approve_new and (packages or wip_packages) %}
<div class="col-sm-6"> <div class="col-sm-6">
<div class="card"> <div class="card">
<h3 class="card-header">{{ _("Packages") }}</h3> <h3 class="card-header">{{ _("Packages") }}</h3>
@ -69,7 +69,7 @@
</div> </div>
{% endif %} {% endif %}
{% if canApproveRel and releases %} {% if can_approve_rel and releases %}
<div class="col-sm-6"> <div class="col-sm-6">
<div class="card"> <div class="card">
<h3 class="card-header">{{ _("Releases") }}</h3> <h3 class="card-header">{{ _("Releases") }}</h3>
@ -159,7 +159,7 @@
<h2 class="mt-5">{{ _("WIP") }}</h2> <h2 class="mt-5">{{ _("WIP") }}</h2>
{% if canApproveNew and (packages or wip_packages) %} {% if can_approve_new and (packages or wip_packages) %}
<div class="card"> <div class="card">
<h3 class="card-header">WIP Packages</h3> <h3 class="card-header">WIP Packages</h3>
<div class="list-group list-group-flush" style="max-height: 300px; overflow: hidden auto;"> <div class="list-group list-group-flush" style="max-height: 300px; overflow: hidden auto;">
@ -188,7 +188,7 @@
<div class="mt-5"></div> <div class="mt-5"></div>
{% if current_user.rank.atLeast(current_user.rank.MODERATOR) %} {% if current_user.rank.at_least(current_user.rank.MODERATOR) %}
<a class="btn btn-secondary float-right" href="{{ url_for('admin.audit') }}"> <a class="btn btn-secondary float-right" href="{{ url_for('admin.audit') }}">
{{ _("View All") }} {{ _("View All") }}
</a> </a>

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block container %} {% block container %}
{% if current_user.rank.atLeast(current_user.rank.APPROVER) %} {% if current_user.rank.at_least(current_user.rank.APPROVER) %}
<nav class="pt-4 tabs-container"> <nav class="pt-4 tabs-container">
<div class="container"> <div class="container">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
@ -47,7 +47,7 @@
{% endif %} {% endif %}
<main class="container mt-5"> <main class="container mt-5">
{% if not current_user.rank.atLeast(current_user.rank.APPROVER) %} {% if not current_user.rank.at_least(current_user.rank.APPROVER) %}
<h1 class="mb-5">{{ self.title() }}</h1> <h1 class="mb-5">{{ self.title() }}</h1>
{% endif %} {% endif %}

@ -22,7 +22,7 @@ Topics to be Added
</div> </div>
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
{% if current_user.rank.atLeast(current_user.rank.APPROVER) %} {% if current_user.rank.at_least(current_user.rank.APPROVER) %}
{% if n >= 10000 %} {% if n >= 10000 %}
<a class="btn btn-secondary" <a class="btn btn-secondary"
href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=100, sort=sort_by) }}"> href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=100, sort=sort_by) }}">

@ -68,7 +68,7 @@
</tr> </tr>
</table> </table>
{% if current_user.rank.atLeast(current_user.rank.MODERATOR) %} {% if current_user.rank.at_least(current_user.rank.MODERATOR) %}
<a class="btn btn-secondary float-right" href="{{ url_for('admin.audit', username=user.username) }}"> <a class="btn btn-secondary float-right" href="{{ url_for('admin.audit', username=user.username) }}">
{{ _("View All") }} {{ _("View All") }}
</a> </a>
@ -81,7 +81,7 @@
<h3>{{ _("Account Deletion and Deactivation") }}</h3> <h3>{{ _("Account Deletion and Deactivation") }}</h3>
{% if current_user.rank.atLeast(current_user.rank.ADMIN) %} {% if current_user.rank.at_least(current_user.rank.ADMIN) %}
<a class="btn btn-danger" href="{{ url_for('users.delete', username=user.username) }}"> <a class="btn btn-danger" href="{{ url_for('users.delete', username=user.username) }}">
{{ _("Delete or Deactivate") }}</a> {{ _("Delete or Deactivate") }}</a>
{% else %} {% else %}

@ -36,7 +36,7 @@
name="deactivate" value="{{ _('Deactivate') }}" name="deactivate" value="{{ _('Deactivate') }}"
{% endif %} {% endif %}
class="btn btn-danger" /> class="btn btn-danger" />
{% if not can_delete and current_user.rank.atLeast(current_user.rank.ADMIN) %} {% if not can_delete and current_user.rank.at_least(current_user.rank.ADMIN) %}
<input type="submit" name="delete" value="{{ _('Delete Anyway') }}" class="btn btn-danger ml-3" /> <input type="submit" name="delete" value="{{ _('Delete Anyway') }}" class="btn btn-danger ml-3" />
{% endif %} {% endif %}
</div> </div>

@ -39,7 +39,7 @@
<p class="text-danger">{{ _("Doesn't have password") }}</p> <p class="text-danger">{{ _("Doesn't have password") }}</p>
{% endif %} {% endif %}
{% if not user.rank.atLeast(current_user.rank) %} {% if not user.rank.at_least(current_user.rank) %}
<h3>{{ _("Ban") }}</h3> <h3>{{ _("Ban") }}</h3>
{% if user.ban %} {% if user.ban %}
<p> <p>

@ -28,8 +28,8 @@
{{ _("Report") }} {{ _("Report") }}
</a> </a>
{% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) %} {% if current_user.is_authenticated and current_user.rank.at_least(current_user.rank.MODERATOR) %}
{% if not user.rank.atLeast(current_user.rank) %} {% if not user.rank.at_least(current_user.rank) %}
<a class="btn btn-secondary float-right mr-3" href="{{ url_for('users.modtools', username=user.username) }}"> <a class="btn btn-secondary float-right mr-3" href="{{ url_for('users.modtools', username=user.username) }}">
<i class="fas fa-user-shield mr-1"></i> <i class="fas fa-user-shield mr-1"></i>
{{ _("Moderator Tools") }} {{ _("Moderator Tools") }}
@ -164,7 +164,7 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% if current_user == user or (current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.ADMIN)) %} {% if current_user == user or (current_user.is_authenticated and current_user.rank.at_least(current_user.rank.ADMIN)) %}
{% for medal in medals_locked %} {% for medal in medals_locked %}
{% set value = medal.progress[0] %} {% set value = medal.progress[0] %}
{% set target = medal.progress[1] %} {% set target = medal.progress[1] %}
@ -202,7 +202,7 @@
{{ _("Create package") }} {{ _("Create package") }}
</a> </a>
{% endif %} {% endif %}
{% if current_user == user or (current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.EDITOR)) %} {% if current_user == user or (current_user.is_authenticated and current_user.rank.at_least(current_user.rank.EDITOR)) %}
<a class="float-right btn btn-sm btn-secondary mr-2" <a class="float-right btn btn-sm btn-secondary mr-2"
href="{{ url_for('todo.tags', author=user.username) }}"> href="{{ url_for('todo.tags', author=user.username) }}">
{{ _("View list of tags") }} {{ _("View list of tags") }}

@ -33,27 +33,27 @@ def is_username_valid(username):
re.match(r"^[A-Za-z0-9._-]*$", username) and not re.match(r"^\.*$", username) re.match(r"^[A-Za-z0-9._-]*$", username) and not re.match(r"^\.*$", username)
def isYes(val): def is_yes(val):
return val and val.lower() in YESES return val and val.lower() in YESES
def isNo(val): def is_no(val):
return val and not isYes(val) return val and not is_yes(val)
def nonEmptyOrNone(str): def nonempty_or_none(str):
if str is None or str == "": if str is None or str == "":
return None return None
return str return str
def shouldReturnJson(): def should_return_json():
return "application/json" in request.accept_mimetypes and \ return "application/json" in request.accept_mimetypes and \
not "text/html" in request.accept_mimetypes not "text/html" in request.accept_mimetypes
def randomString(n): def random_string(n):
return secrets.token_hex(int(n / 2)) return secrets.token_hex(int(n / 2))

@ -26,7 +26,7 @@ from urllib.parse import urlsplit
from git import GitCommandError from git import GitCommandError
from app.tasks import TaskError from app.tasks import TaskError
from app.utils import randomString from app.utils import random_string
def generate_git_url(urlstr): def generate_git_url(urlstr):
@ -40,7 +40,7 @@ def generate_git_url(urlstr):
@contextlib.contextmanager @contextlib.contextmanager
def get_temp_dir(): def get_temp_dir():
temp = os.path.join(tempfile.gettempdir(), randomString(10)) temp = os.path.join(tempfile.gettempdir(), random_string(10))
yield temp yield temp
shutil.rmtree(temp) shutil.rmtree(temp)
@ -50,21 +50,21 @@ def get_temp_dir():
# Throws `TaskError` on failure. # Throws `TaskError` on failure.
# Caller is responsible for deleting returned directory. # Caller is responsible for deleting returned directory.
@contextlib.contextmanager @contextlib.contextmanager
def clone_repo(urlstr, ref=None, recursive=False): def clone_repo(url_str, ref=None, recursive=False):
gitDir = os.path.join(tempfile.gettempdir(), randomString(10)) git_dir = os.path.join(tempfile.gettempdir(), random_string(10))
try: try:
gitUrl = generate_git_url(urlstr) git_url = generate_git_url(url_str)
print("Cloning from " + gitUrl) print("Cloning from " + git_url)
if ref is None: if ref is None:
repo = git.Repo.clone_from(gitUrl, gitDir, repo = git.Repo.clone_from(git_url, git_dir,
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15) progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15)
else: else:
assert ref != "" assert ref != ""
repo = git.Repo.init(gitDir) repo = git.Repo.init(git_dir)
origin = repo.create_remote("origin", url=gitUrl) origin = repo.create_remote("origin", url=git_url)
assert origin.exists() assert origin.exists()
origin.fetch() origin.fetch()
repo.git.checkout(ref) repo.git.checkout(ref)
@ -72,7 +72,7 @@ def clone_repo(urlstr, ref=None, recursive=False):
repo.git.submodule('update', '--init') repo.git.submodule('update', '--init')
yield repo yield repo
shutil.rmtree(gitDir) shutil.rmtree(git_dir)
return return
except GitCommandError as e: except GitCommandError as e:
@ -83,7 +83,7 @@ def clone_repo(urlstr, ref=None, recursive=False):
err = "Unable to find the reference " + (ref or "?") + "\n" + e.stderr err = "Unable to find the reference " + (ref or "?") + "\n" + e.stderr
raise TaskError(err.replace("stderr: ", "") \ raise TaskError(err.replace("stderr: ", "") \
.replace("Cloning into '" + gitDir + "'...", "") \ .replace("Cloning into '" + git_dir + "'...", "") \
.strip()) .strip())

@ -27,7 +27,7 @@ from sqlalchemy.orm import sessionmaker
from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, ThreadReply, Thread, PackageState, PackageType, PackageAlias from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, ThreadReply, Thread, PackageState, PackageType, PackageAlias
def getPackageByInfo(author, name): def get_package_by_info(author, name):
user = User.query.filter_by(username=author).first() user = User.query.filter_by(username=author).first()
if user is None: if user is None:
return None return None
@ -39,6 +39,7 @@ def getPackageByInfo(author, name):
return package return package
def is_package_page(f): def is_package_page(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
@ -48,9 +49,9 @@ def is_package_page(f):
author = kwargs["author"] author = kwargs["author"]
name = kwargs["name"] name = kwargs["name"]
package = getPackageByInfo(author, name) package = get_package_by_info(author, name)
if package is None: if package is None:
package = getPackageByInfo(author, name + "_game") package = get_package_by_info(author, name + "_game")
if package and package.type == PackageType.GAME: if package and package.type == PackageType.GAME:
args = dict(kwargs) args = dict(kwargs)
args["name"] = name + "_game" args["name"] = name + "_game"
@ -72,28 +73,28 @@ def is_package_page(f):
return decorated_function return decorated_function
def addNotification(target, causer: User, type: NotificationType, title: str, url: str, package: Package = None): def add_notification(target, causer: User, type: NotificationType, title: str, url: str, package: Package = None):
try: try:
iter(target) iter(target)
for x in target: for x in target:
addNotification(x, causer, type, title, url, package) add_notification(x, causer, type, title, url, package)
return return
except TypeError: except TypeError:
pass pass
if target.rank.atLeast(UserRank.NEW_MEMBER) and target != causer: if target.rank.at_least(UserRank.NEW_MEMBER) and target != causer:
Notification.query.filter_by(user=target, causer=causer, type=type, title=title, url=url, package=package).delete() Notification.query.filter_by(user=target, causer=causer, type=type, title=title, url=url, package=package).delete()
notif = Notification(target, causer, type, title, url, package) notif = Notification(target, causer, type, title, url, package)
db.session.add(notif) db.session.add(notif)
def addAuditLog(severity: AuditSeverity, causer: User, title: str, url: typing.Optional[str], def add_audit_log(severity: AuditSeverity, causer: User, title: str, url: typing.Optional[str],
package: Package = None, description: str = None): package: Package = None, description: str = None):
entry = AuditLogEntry(causer, severity, title, url, package, description) entry = AuditLogEntry(causer, severity, title, url, package, description)
db.session.add(entry) db.session.add(entry)
def clearNotifications(url): def clear_notifications(url):
if current_user.is_authenticated: if current_user.is_authenticated:
Notification.query.filter_by(user=current_user, url=url).delete() Notification.query.filter_by(user=current_user, url=url).delete()
db.session.commit() db.session.commit()
@ -105,12 +106,12 @@ def get_system_user():
return system_user return system_user
def addSystemNotification(target, type: NotificationType, title: str, url: str, package: Package = None): def add_system_notification(target, type: NotificationType, title: str, url: str, package: Package = None):
return addNotification(target, get_system_user(), type, title, url, package) return add_notification(target, get_system_user(), type, title, url, package)
def addSystemAuditLog(severity: AuditSeverity, title: str, url: str, package=None, description=None): def add_system_audit_log(severity: AuditSeverity, title: str, url: str, package=None, description=None):
return addAuditLog(severity, get_system_user(), title, url, package, description) return add_audit_log(severity, get_system_user(), title, url, package, description)
def post_bot_message(package: Package, title: str, message: str): def post_bot_message(package: Package, title: str, message: str):
@ -133,8 +134,7 @@ def post_bot_message(package: Package, title: str, message: str):
reply.comment = "**{}**\n\n{}\n\nThis is an automated message, but you can reply if you need help".format(title, message) reply.comment = "**{}**\n\n{}\n\nThis is an automated message, but you can reply if you need help".format(title, message)
db.session.add(reply) db.session.add(reply)
addNotification(thread.watchers, system_user, NotificationType.BOT, add_notification(thread.watchers, system_user, NotificationType.BOT, title, thread.get_view_url(), thread.package)
title, thread.get_view_url(), thread.package)
thread.replies.append(reply) thread.replies.append(reply)

@ -12,9 +12,10 @@ from urllib.parse import urlencode
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
def urlEncodeNonAscii(b): def url_encode_non_ascii(b):
return re.sub('[\x80-\xFF]', lambda c: '%%%02x' % ord(c.group(0)), b) return re.sub('[\x80-\xFF]', lambda c: '%%%02x' % ord(c.group(0)), b)
class Profile: class Profile:
def __init__(self, username): def __init__(self, username):
self.username = username self.username = username
@ -31,6 +32,7 @@ class Profile:
def __str__(self): def __str__(self):
return self.username + "\n" + str(self.signature) + "\n" + str(self.properties) return self.username + "\n" + str(self.signature) + "\n" + str(self.properties)
def __extract_properties(profile, soup): def __extract_properties(profile, soup):
el = soup.find(id="viewprofile") el = soup.find(id="viewprofile")
if el is None: if el is None:
@ -66,6 +68,7 @@ def __extract_properties(profile, soup):
elif element and element.name is not None: elif element and element.name is not None:
print("Unexpected other") print("Unexpected other")
def __extract_signature(soup): def __extract_signature(soup):
res = soup.find_all("div", class_="signature") res = soup.find_all("div", class_="signature")
if len(res) != 1: if len(res) != 1:
@ -74,7 +77,7 @@ def __extract_signature(soup):
return str(res[0]) return str(res[0])
def getProfileURL(url, username): def get_profile_url(url, username):
url = urlparse.urlparse(url) url = urlparse.urlparse(url)
# Update path # Update path
@ -89,8 +92,8 @@ def getProfileURL(url, username):
return urlparse.urlunparse(url) return urlparse.urlunparse(url)
def getProfile(url, username): def get_profile(url, username):
url = getProfileURL(url, username) url = get_profile_url(url, username)
try: try:
req = urllib.request.urlopen(url, timeout=15) req = urllib.request.urlopen(url, timeout=15)
@ -114,7 +117,8 @@ def getProfile(url, username):
regex_id = re.compile(r"^.*t=([0-9]+).*$") regex_id = re.compile(r"^.*t=([0-9]+).*$")
def parseForumListPage(id, page, out, extra=None):
def parse_forum_list_page(id, page, out, extra=None):
num_per_page = 30 num_per_page = 30
start = page*num_per_page+1 start = page*num_per_page+1
print(" - Fetching page {} (topics {}-{})".format(page, start, start+num_per_page)) print(" - Fetching page {} (topics {}-{})".format(page, start, start+num_per_page))
@ -171,15 +175,11 @@ def parseForumListPage(id, page, out, extra=None):
return True return True
def getTopicsFromForum(id, out, extra=None):
def get_topics_from_forum(id, out, extra=None):
print("Fetching all topics from forum {}".format(id)) print("Fetching all topics from forum {}".format(id))
page = 0 page = 0
while parseForumListPage(id, page, out, extra): while parse_forum_list_page(id, page, out, extra):
page = page + 1 page = page + 1
return out return out
def dumpTitlesToFile(topics, path):
with open(path, "w") as out_file:
for topic in topics.values():
out_file.write(topic["title"] + "\n")

@ -76,7 +76,7 @@ def rank_required(rank):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not current_user.is_authenticated: if not current_user.is_authenticated:
return redirect(url_for("users.login")) return redirect(url_for("users.login"))
if not current_user.rank.atLeast(rank): if not current_user.rank.at_least(rank):
abort(403) abort(403)
return f(*args, **kwargs) return f(*args, **kwargs)