From 347f8e5a2219e4f650f2f7350fd26866bd6359a4 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Sat, 24 Jul 2021 02:30:43 +0100 Subject: [PATCH] Add support for renaming users and package alias redirects Fixes #270 --- app/blueprints/packages/packages.py | 39 +++++++++++++++++ app/blueprints/users/account.py | 6 +++ app/blueprints/users/settings.py | 15 +++++++ app/models/packages.py | 42 ++++++++++++++++++- app/querybuilder.py | 2 +- app/templates/packages/alias_create_edit.html | 23 ++++++++++ app/templates/packages/alias_list.html | 27 ++++++++++++ app/templates/users/account.html | 1 + app/utils/models.py | 20 +++++---- 9 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 app/templates/packages/alias_create_edit.html create mode 100644 app/templates/packages/alias_list.html diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index 54a1300..94d2f75 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -545,3 +545,42 @@ def audit(package): pagination = query.paginate(page, num, True) return render_template("admin/audit.html", log=pagination.items, pagination=pagination) + + +class PackageAliasForm(FlaskForm): + author = StringField("Author Name", [InputRequired(), Length(1, 50)]) + name = StringField("Name (Technical)", [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) + submit = SubmitField("Save") + + +@bp.route("/packages///aliases/") +@rank_required(UserRank.EDITOR) +@is_package_page +def alias_list(package: Package): + return render_template("packages/alias_list.html", package=package) + + +@bp.route("/packages///aliases/new/", methods=["GET", "POST"]) +@bp.route("/packages///aliases//", methods=["GET", "POST"]) +@rank_required(UserRank.EDITOR) +@is_package_page +def alias_create_edit(package: Package, alias_id: int = None): + alias = None + if alias_id: + alias = PackageAlias.query.get(alias_id) + if alias is None or alias.package != package: + abort(404) + + form = PackageAliasForm(request.form, obj=alias) + if form.validate_on_submit(): + if alias is None: + alias = PackageAlias() + alias.package = package + db.session.add(alias) + + form.populate_obj(alias) + db.session.commit() + + return redirect(package.getAliasListURL()) + + return render_template("packages/alias_create_edit.html", package=package, form=form) diff --git a/app/blueprints/users/account.py b/app/blueprints/users/account.py index 6c4de4d..f6d830a 100644 --- a/app/blueprints/users/account.py +++ b/app/blueprints/users/account.py @@ -127,6 +127,12 @@ def handle_register(form): flash("That username/display name is already in use, please choose another.", "danger") return + alias_by_name = PackageAlias.query.filter(or_( + PackageAlias.author==form.username.data, + PackageAlias.author==form.display_name.data)).first() + if alias_by_name: + flash("That username/display name is already in use, please choose another.", "danger") + return user_by_email = User.query.filter_by(email=form.email.data).first() if user_by_email: diff --git a/app/blueprints/users/settings.py b/app/blueprints/users/settings.py index e089691..e89f773 100644 --- a/app/blueprints/users/settings.py +++ b/app/blueprints/users/settings.py @@ -56,6 +56,12 @@ def handle_profile_edit(form, user, username): flash("A user already has that name", "danger") return None + alias_by_name = PackageAlias.query.filter(or_( + PackageAlias.author == form.display_name.data)).first() + if alias_by_name: + flash("A user already has that name", "danger") + return + user.display_name = form.display_name.data severity = AuditSeverity.USER if current_user == user else AuditSeverity.MODERATION @@ -190,6 +196,7 @@ def email_notifications(username=None): class UserAccountForm(FlaskForm): + username = StringField("Username", [Optional(), Length(1, 50)]) display_name = StringField("Display name", [Optional(), Length(2, 100)]) forums_username = StringField("Forums Username", [Optional(), Length(2, 50)]) github_username = StringField("GitHub Username", [Optional(), Length(2, 50)]) @@ -219,6 +226,14 @@ def account(username): # Copy form fields to user_profile fields if user.checkPerm(current_user, Permission.CHANGE_USERNAMES): + if user.username != form.username.data: + for package in user.packages: + alias = PackageAlias(user.username, package.name) + package.aliases.append(alias) + db.session.add(alias) + + user.username = form.username.data + user.display_name = form.display_name.data user.forums_username = nonEmptyOrNone(form.forums_username.data) user.github_username = nonEmptyOrNone(form.github_username.data) diff --git a/app/models/packages.py b/app/models/packages.py index d2e6444..0a77311 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -337,6 +337,9 @@ class Package(db.Model): update_config = db.relationship("PackageUpdateConfig", uselist=False, back_populates="package", cascade="all, delete, delete-orphan") + aliases = db.relationship("PackageAlias", foreign_keys="PackageAlias.package_id", + back_populates="package", cascade="all, delete, delete-orphan") + def __init__(self, package=None): if package is None: return @@ -385,16 +388,22 @@ class Package(db.Model): release = self.getDownloadRelease(version=version) release_id = release and release.id - return { + ret = { "name": self.name, "title": self.title, "author": self.author.username, "short_description": self.short_desc, "type": self.type.toName(), "release": release_id, - "thumbnail": (base_url + tnurl) if tnurl is not None else None + "thumbnail": (base_url + tnurl) if tnurl is not None else None, + "aliases": [ alias.getAsDictionary() for alias in self.aliases ], } + if not ret["aliases"]: + del ret["aliases"] + + return ret + def getAsDictionary(self, base_url, version=None): tnurl = self.getThumbnailURL(1) release = self.getDownloadRelease(version=version) @@ -543,6 +552,14 @@ class Package(db.Model): return None + def getAliasListURL(self): + return url_for("packages.alias_list", + author=self.author.username, name=self.name) + + def getAliasCreateURL(self): + return url_for("packages.alias_create_edit", + author=self.author.username, name=self.name) + def checkPerm(self, user, perm): if not user.is_authenticated: return False @@ -1033,3 +1050,24 @@ class PackageUpdateConfig(db.Model): def get_create_release_url(self): return self.package.getCreateReleaseURL(title=self.get_title(), ref=self.get_ref()) + + +class PackageAlias(db.Model): + id = db.Column(db.Integer, primary_key=True) + + package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False) + package = db.relationship("Package", back_populates="aliases", foreign_keys=[package_id]) + + author = db.Column(db.String(50), nullable=False) + name = db.Column(db.String(100), nullable=False) + + def __init__(self, author="", name=""): + self.author = author + self.name = name + + def getEditURL(self): + return url_for("packages.alias_create_edit", author=self.package.author.username, + name=self.package.name, alias_id=self.id) + + def getAsDictionary(self): + return f"{self.author}/{self.name}" diff --git a/app/querybuilder.py b/app/querybuilder.py index 6c05fac..3ae5f70 100644 --- a/app/querybuilder.py +++ b/app/querybuilder.py @@ -101,7 +101,7 @@ class QueryBuilder: else: query = Package.query.filter_by(state=PackageState.APPROVED) - query = query.options(subqueryload(Package.main_screenshot)) + query = query.options(subqueryload(Package.main_screenshot), subqueryload(Package.aliases)) query = self.orderPackageQuery(self.filterPackageQuery(query)) diff --git a/app/templates/packages/alias_create_edit.html b/app/templates/packages/alias_create_edit.html new file mode 100644 index 0000000..b076000 --- /dev/null +++ b/app/templates/packages/alias_create_edit.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %} + {{ _("Alias") }} +{% endblock %} + +{% block link %} + {{ package.title }} +{% endblock %} + +{% block content %} +Back to Aliases + +{% from "macros/forms.html" import render_field, render_submit_field, render_toggle_field %} +
+ {{ form.hidden_tag() }} + + {{ render_field(form.author) }} + {{ render_field(form.name) }} + {{ render_submit_field(form.submit) }} +
+ +{% endblock %} diff --git a/app/templates/packages/alias_list.html b/app/templates/packages/alias_list.html new file mode 100644 index 0000000..8dc06f9 --- /dev/null +++ b/app/templates/packages/alias_list.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %} + {{ _("Aliases") }} +{% endblock %} + +{% block link %} + {{ package.title }} +{% endblock %} + +{% block content %} +Create +

{{ _("Aliases for %(title)s by %(author)s", title=self.link(), author=package.author.display_name) }}

+ +
+ {% for alias in package.aliases %} + + {{ alias.author }} / {{ alias.name }} + + {% else %} +
+ No aliases +
+ {% endfor %} +
+ +{% endblock %} diff --git a/app/templates/users/account.html b/app/templates/users/account.html index edaddd3..fb3f13f 100644 --- a/app/templates/users/account.html +++ b/app/templates/users/account.html @@ -18,6 +18,7 @@ {{ form.hidden_tag() }} {% if user.checkPerm(current_user, "CHANGE_USERNAMES") %} + {{ render_field(form.username, tabindex=230) }} {{ render_field(form.display_name, tabindex=230) }} {{ render_field(form.forums_username, tabindex=230) }} {{ render_field_prefix(form.github_username, tabindex=230) }} diff --git a/app/utils/models.py b/app/utils/models.py index d2bbf80..30dc03d 100644 --- a/app/utils/models.py +++ b/app/utils/models.py @@ -18,7 +18,7 @@ from functools import wraps from flask import abort, redirect, url_for, request from flask_login import current_user -from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, ThreadReply, Thread, PackageState, PackageType +from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, ThreadReply, Thread, PackageState, PackageType, PackageAlias def getPackageByInfo(author, name): @@ -45,16 +45,22 @@ def is_package_page(f): package = getPackageByInfo(author, name) if package is None: package = getPackageByInfo(author, name + "_game") - if package is None or package.type != PackageType.GAME: - abort(404) + if package and package.type == PackageType.GAME: + args = dict(kwargs) + args["name"] = name + "_game" + return redirect(url_for(request.endpoint, **args)) - args = dict(kwargs) - args["name"] = name + "_game" - return redirect(url_for(request.endpoint, **args)) + alias = PackageAlias.query.filter_by(author=author, name=name).first() + if alias is not None: + args = dict(kwargs) + args["author"] = alias.package.author.username + args["name"] = alias.package.name + return redirect(url_for(request.endpoint, **args)) + + abort(404) del kwargs["author"] del kwargs["name"] - return f(package=package, *args, **kwargs) return decorated_function