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

View File

@ -545,3 +545,42 @@ def audit(package):
pagination = query.paginate(page, num, True) pagination = query.paginate(page, num, True)
return render_template("admin/audit.html", log=pagination.items, pagination=pagination) 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)

View File

@ -127,6 +127,12 @@ def handle_register(form):
flash("That username/display name is already in use, please choose another.", "danger") flash("That username/display name is already in use, please choose another.", "danger")
return 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() user_by_email = User.query.filter_by(email=form.email.data).first()
if user_by_email: if user_by_email:

View File

@ -56,6 +56,12 @@ def handle_profile_edit(form, user, username):
flash("A user already has that name", "danger") flash("A user already has that name", "danger")
return None 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 user.display_name = form.display_name.data
severity = AuditSeverity.USER if current_user == user else AuditSeverity.MODERATION severity = AuditSeverity.USER if current_user == user else AuditSeverity.MODERATION
@ -190,6 +196,7 @@ def email_notifications(username=None):
class UserAccountForm(FlaskForm): class UserAccountForm(FlaskForm):
username = StringField("Username", [Optional(), Length(1, 50)])
display_name = StringField("Display name", [Optional(), Length(2, 100)]) display_name = StringField("Display name", [Optional(), Length(2, 100)])
forums_username = StringField("Forums Username", [Optional(), Length(2, 50)]) forums_username = StringField("Forums Username", [Optional(), Length(2, 50)])
github_username = StringField("GitHub 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 # Copy form fields to user_profile fields
if user.checkPerm(current_user, Permission.CHANGE_USERNAMES): 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.display_name = form.display_name.data
user.forums_username = nonEmptyOrNone(form.forums_username.data) user.forums_username = nonEmptyOrNone(form.forums_username.data)
user.github_username = nonEmptyOrNone(form.github_username.data) user.github_username = nonEmptyOrNone(form.github_username.data)

View File

@ -337,6 +337,9 @@ class Package(db.Model):
update_config = db.relationship("PackageUpdateConfig", uselist=False, back_populates="package", update_config = db.relationship("PackageUpdateConfig", uselist=False, back_populates="package",
cascade="all, delete, delete-orphan") 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): def __init__(self, package=None):
if package is None: if package is None:
return return
@ -385,16 +388,22 @@ class Package(db.Model):
release = self.getDownloadRelease(version=version) release = self.getDownloadRelease(version=version)
release_id = release and release.id release_id = release and release.id
return { ret = {
"name": self.name, "name": self.name,
"title": self.title, "title": self.title,
"author": self.author.username, "author": self.author.username,
"short_description": self.short_desc, "short_description": self.short_desc,
"type": self.type.toName(), "type": self.type.toName(),
"release": release_id, "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): def getAsDictionary(self, base_url, version=None):
tnurl = self.getThumbnailURL(1) tnurl = self.getThumbnailURL(1)
release = self.getDownloadRelease(version=version) release = self.getDownloadRelease(version=version)
@ -543,6 +552,14 @@ class Package(db.Model):
return None 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): def checkPerm(self, user, perm):
if not user.is_authenticated: if not user.is_authenticated:
return False return False
@ -1033,3 +1050,24 @@ class PackageUpdateConfig(db.Model):
def get_create_release_url(self): def get_create_release_url(self):
return self.package.getCreateReleaseURL(title=self.get_title(), ref=self.get_ref()) 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}"

View File

@ -101,7 +101,7 @@ class QueryBuilder:
else: else:
query = Package.query.filter_by(state=PackageState.APPROVED) 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)) query = self.orderPackageQuery(self.filterPackageQuery(query))

View File

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

View File

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

View File

@ -18,6 +18,7 @@
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% if user.checkPerm(current_user, "CHANGE_USERNAMES") %} {% if user.checkPerm(current_user, "CHANGE_USERNAMES") %}
{{ render_field(form.username, tabindex=230) }}
{{ render_field(form.display_name, tabindex=230) }} {{ render_field(form.display_name, tabindex=230) }}
{{ render_field(form.forums_username, tabindex=230) }} {{ render_field(form.forums_username, tabindex=230) }}
{{ render_field_prefix(form.github_username, tabindex=230) }} {{ render_field_prefix(form.github_username, tabindex=230) }}

View File

@ -18,7 +18,7 @@
from functools import wraps from functools import wraps
from flask import abort, redirect, url_for, request from flask import abort, redirect, url_for, request
from flask_login import current_user 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): def getPackageByInfo(author, name):
@ -45,16 +45,22 @@ def is_package_page(f):
package = getPackageByInfo(author, name) package = getPackageByInfo(author, name)
if package is None: if package is None:
package = getPackageByInfo(author, name + "_game") package = getPackageByInfo(author, name + "_game")
if package is None or package.type != PackageType.GAME: if package and package.type == PackageType.GAME:
abort(404) args = dict(kwargs)
args["name"] = name + "_game"
return redirect(url_for(request.endpoint, **args))
args = dict(kwargs) alias = PackageAlias.query.filter_by(author=author, name=name).first()
args["name"] = name + "_game" if alias is not None:
return redirect(url_for(request.endpoint, **args)) 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["author"]
del kwargs["name"] del kwargs["name"]
return f(package=package, *args, **kwargs) return f(package=package, *args, **kwargs)
return decorated_function return decorated_function