Add support for renaming users and package alias redirects

Fixes #270
This commit is contained in:
rubenwardy 2021-07-24 02:30:43 +01:00
parent 0614e6b28b
commit 347f8e5a22
9 changed files with 165 additions and 10 deletions

@ -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/<author>/<name>/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/<author>/<name>/aliases/new/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/aliases/<int:alias_id>/", 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)

@ -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:

@ -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)

@ -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}"

@ -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))

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}
{{ _("Alias") }}
{% endblock %}
{% block link %}
<a href="{{ package.getDetailsURL() }}">{{ package.title }}</a>
{% endblock %}
{% block content %}
<a class="btn btn-secondary" href="{{ package.getAliasListURL() }}">Back to Aliases</a>
{% from "macros/forms.html" import render_field, render_submit_field, render_toggle_field %}
<form method="POST" action="" enctype="multipart/form-data" class="mt-4">
{{ form.hidden_tag() }}
{{ render_field(form.author) }}
{{ render_field(form.name) }}
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}
{{ _("Aliases") }}
{% endblock %}
{% block link %}
<a href="{{ package.getDetailsURL() }}">{{ package.title }}</a>
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ package.getAliasCreateURL() }}">Create</a>
<h1>{{ _("Aliases for %(title)s by %(author)s", title=self.link(), author=package.author.display_name) }}</h1>
<div class="list-group">
{% for alias in package.aliases %}
<a class="list-group-item list-group-item-action" href="{{ alias.getEditURL() }}">
{{ alias.author }} / {{ alias.name }}
</a>
{% else %}
<div class="list-group-item">
No aliases
</div>
{% endfor %}
</div>
{% endblock %}

@ -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) }}

@ -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