mirror of
https://github.com/minetest/contentdb.git
synced 2024-11-09 17:13:45 +01:00
Add user deletion / deactivation
This commit is contained in:
parent
fd0b203f1e
commit
778a602aa6
@ -137,7 +137,6 @@ def view(package):
|
||||
.all()
|
||||
|
||||
releases = getReleases(package)
|
||||
requests = [r for r in package.requests if r.status == 0]
|
||||
|
||||
review_thread = package.review_thread
|
||||
if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||
@ -171,7 +170,7 @@ def view(package):
|
||||
has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0
|
||||
|
||||
return render_template("packages/view.html",
|
||||
package=package, releases=releases, requests=requests,
|
||||
package=package, releases=releases,
|
||||
alternatives=alternatives, similar_topics=similar_topics,
|
||||
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl,
|
||||
threads=threads.all(), has_review=has_review)
|
||||
|
@ -1,11 +1,11 @@
|
||||
from flask import *
|
||||
from flask_login import current_user, login_required
|
||||
from flask_login import current_user, login_required, logout_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.models import *
|
||||
from app.utils import nonEmptyOrNone, addAuditLog, randomString
|
||||
from app.utils import nonEmptyOrNone, addAuditLog, randomString, rank_required
|
||||
from app.tasks.emails import send_verify_email
|
||||
from . import bp
|
||||
|
||||
@ -186,3 +186,38 @@ def email_notifications(username=None):
|
||||
return render_template("users/settings_email.html",
|
||||
form=form, user=user, types=types, is_new=is_new,
|
||||
tabs=get_setting_tabs(user), current_tab="notifications")
|
||||
|
||||
|
||||
@bp.route("/users/<username>/delete/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def delete(username):
|
||||
user: User = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if request.method == "GET":
|
||||
return render_template("users/delete.html", user=user, can_delete=user.can_delete())
|
||||
|
||||
if user.can_delete():
|
||||
msg = "Deleted user {}".format(user.username)
|
||||
flash(msg, "success")
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user, msg, None)
|
||||
|
||||
db.session.delete(user)
|
||||
else:
|
||||
user.replies.delete()
|
||||
for thread in user.threads.all():
|
||||
db.session.delete(thread)
|
||||
user.email = None
|
||||
user.rank = UserRank.NOT_JOINED
|
||||
|
||||
msg = "Deactivated user {}".format(user.username)
|
||||
flash(msg, "success")
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user, msg, None)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if user == current_user:
|
||||
logout_user()
|
||||
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
@ -166,16 +166,20 @@ class User(db.Model, UserMixin):
|
||||
donate_url = db.Column(db.String(255), nullable=True, default=None)
|
||||
|
||||
# Content
|
||||
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id",
|
||||
order_by=desc(text("Notification.created_at")))
|
||||
notifications = db.relationship("Notification", foreign_keys="Notification.user_id",
|
||||
order_by=desc(text("Notification.created_at")), back_populates="user", cascade="all, delete, delete-orphan")
|
||||
caused_notifications = db.relationship("Notification", foreign_keys="Notification.causer_id",
|
||||
back_populates="causer", cascade="all, delete, delete-orphan")
|
||||
notification_preferences = db.relationship("UserNotificationPreferences", uselist=False, back_populates="user",
|
||||
cascade="all, delete, delete-orphan")
|
||||
|
||||
notification_preferences = db.relationship("UserNotificationPreferences", uselist=False, back_populates="user")
|
||||
audit_log_entries = db.relationship("AuditLogEntry", foreign_keys="AuditLogEntry.causer_id", back_populates="causer")
|
||||
|
||||
packages = db.relationship("Package", back_populates="author", lazy="dynamic")
|
||||
reviews = db.relationship("PackageReview", back_populates="author", order_by=db.desc("review_created_at"))
|
||||
tokens = db.relationship("APIToken", back_populates="owner", lazy="dynamic")
|
||||
threads = db.relationship("Thread", back_populates="author", lazy="dynamic")
|
||||
replies = db.relationship("ThreadReply", back_populates="author", lazy="dynamic")
|
||||
reviews = db.relationship("PackageReview", back_populates="author", order_by=db.desc("package_review_created_at"), cascade="all, delete, delete-orphan")
|
||||
tokens = db.relationship("APIToken", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
threads = db.relationship("Thread", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
replies = db.relationship("ThreadReply", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
|
||||
def __init__(self, username=None, active=False, email=None, password=None):
|
||||
self.username = username
|
||||
@ -269,6 +273,9 @@ class User(db.Model, UserMixin):
|
||||
self.checkPerm(current_user, Permission.CHANGE_EMAIL) or \
|
||||
self.checkPerm(current_user, Permission.CHANGE_RANK)
|
||||
|
||||
def can_delete(self):
|
||||
return self.packages.count() == 0 and ForumTopic.query.filter_by(author=self).count() == 0
|
||||
|
||||
|
||||
class UserEmailVerification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -367,10 +374,10 @@ class Notification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
user = db.relationship("User", foreign_keys=[user_id])
|
||||
user = db.relationship("User", foreign_keys=[user_id], back_populates="notifications")
|
||||
|
||||
causer_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
causer = db.relationship("User", foreign_keys=[causer_id])
|
||||
causer = db.relationship("User", foreign_keys=[causer_id], back_populates="caused_notifications")
|
||||
|
||||
type = db.Column(db.Enum(NotificationType), nullable=False, default=NotificationType.OTHER)
|
||||
|
||||
@ -705,7 +712,7 @@ class Package(db.Model):
|
||||
downloads = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
review_thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True, default=None)
|
||||
review_thread = db.relationship("Thread", foreign_keys=[review_thread_id])
|
||||
review_thread = db.relationship("Thread", foreign_keys=[review_thread_id], back_populates="is_review_thread")
|
||||
|
||||
# Downloads
|
||||
repo = db.Column(db.String(200), nullable=True)
|
||||
@ -734,7 +741,7 @@ class Package(db.Model):
|
||||
maintainers = db.relationship("User", secondary=maintainers, lazy="subquery")
|
||||
|
||||
threads = db.relationship("Thread", back_populates="package", order_by=db.desc("thread_created_at"), foreign_keys="Thread.package_id")
|
||||
reviews = db.relationship("PackageReview", back_populates="package", order_by=db.desc("review_created_at"))
|
||||
reviews = db.relationship("PackageReview", back_populates="package", order_by=db.desc("package_review_created_at"))
|
||||
|
||||
def __init__(self, package=None):
|
||||
if package is None:
|
||||
@ -1337,10 +1344,12 @@ class Thread(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id])
|
||||
package = db.relationship("Package", foreign_keys=[package_id], back_populates="threads")
|
||||
|
||||
is_review_thread = db.relationship("Package", foreign_keys=[Package.review_thread_id], back_populates="review_thread")
|
||||
|
||||
review_id = db.Column(db.Integer, db.ForeignKey("package_review.id"), nullable=True)
|
||||
review = db.relationship("PackageReview", foreign_keys=[review_id])
|
||||
review = db.relationship("PackageReview", foreign_keys=[review_id], cascade="all, delete")
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
author = db.relationship("User", back_populates="threads", foreign_keys=[author_id])
|
||||
@ -1438,7 +1447,7 @@ class PackageReview(db.Model):
|
||||
|
||||
recommends = db.Column(db.Boolean, nullable=False)
|
||||
|
||||
thread = db.relationship("Thread", uselist=False, back_populates="review")
|
||||
thread = db.relationship("Thread", uselist=False, back_populates="review", cascade="all, delete")
|
||||
|
||||
def asSign(self):
|
||||
return 1 if self.recommends else -1
|
||||
@ -1473,14 +1482,13 @@ class AuditSeverity(enum.Enum):
|
||||
return item if type(item) == AuditSeverity else AuditSeverity[item]
|
||||
|
||||
|
||||
|
||||
class AuditLogEntry(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
causer_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
causer = db.relationship("User", foreign_keys=[causer_id])
|
||||
causer_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
|
||||
causer = db.relationship("User", back_populates="", foreign_keys=[causer_id])
|
||||
|
||||
severity = db.Column(db.Enum(AuditSeverity), nullable=False)
|
||||
|
||||
|
@ -32,12 +32,16 @@ Audit Log
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2 text-muted">
|
||||
<img
|
||||
class="img-fluid user-photo img-thumbnail img-thumbnail-1"
|
||||
style="max-height: 22px;"
|
||||
src="{{ entry.causer.getProfilePicURL() }}" />
|
||||
{% if entry.causer %}
|
||||
<img
|
||||
class="img-fluid user-photo img-thumbnail img-thumbnail-1"
|
||||
style="max-height: 22px;"
|
||||
src="{{ entry.causer.getProfilePicURL() }}" />
|
||||
|
||||
<span class="pl-2">{{ entry.causer.display_name }}</span>
|
||||
<span class="pl-2">{{ entry.causer.display_name }}</span>
|
||||
{% else %}
|
||||
<i>Deleted User</i>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm">
|
||||
|
@ -10,9 +10,16 @@
|
||||
{% endif %}
|
||||
|
||||
<h1>{{ entry.title }}</h1>
|
||||
<p class="text-muted mb-4">
|
||||
{{ _("Caused by %(author)s.", author=entry.causer.display_name) }}
|
||||
</p>
|
||||
|
||||
{% if entry.causer %}
|
||||
<p class="text-muted mb-4">
|
||||
{{ _("Caused by %(author)s.", author=entry.causer.display_name) }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-muted mb-4">
|
||||
{{ _("Caused by a deleted user.", author=entry.causer.display_name) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<pre><code>{{ entry.description }}</code></pre>
|
||||
|
||||
|
32
app/templates/users/delete.html
Normal file
32
app/templates/users/delete.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Delete user {{ user.display_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST" action="" class="card box_grey">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
|
||||
<h3 class="card-header">{{ self.title() }}</h3>
|
||||
<div class="card-body">
|
||||
<p><b>Deleting is permanent</b></p>
|
||||
|
||||
{% if can_delete %}
|
||||
<p>
|
||||
{{ _("This will delete your account, removing %(threads)d threads and %(replies)d replies.",
|
||||
threads=user.threads.count(), replies=user.replies.count()) }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{{ _("As you have packages and/or forum threads, your account can be fully deleted.") }}
|
||||
{{ _("Instead, your account will be deactivated and all personal information wiped - including %(threads)d threads and %(replies)d replies.",
|
||||
threads=user.threads.count(), replies=user.replies.count()) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-secondary mr-3" href="{{ url_for('users.profile_edit', username=user.username) }}">Cancel</a>
|
||||
<input type="submit" value="{% if can_delete %}Delete{% else %}Deactivate{% endif %}" class="btn btn-danger" />
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
28
migrations/versions/43dc7dbf64c8_.py
Normal file
28
migrations/versions/43dc7dbf64c8_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 43dc7dbf64c8
|
||||
Revises: c1ea65e2b492
|
||||
Create Date: 2020-12-09 19:06:11.891807
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '43dc7dbf64c8'
|
||||
down_revision = 'c1ea65e2b492'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('audit_log_entry', 'causer_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('audit_log_entry', 'causer_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
5
utils/downgrade_migration.sh
Executable file
5
utils/downgrade_migration.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Create a database migration, and copy it back to the host.
|
||||
|
||||
docker exec contentdb_app_1 sh -c "FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db downgrade"
|
Loading…
Reference in New Issue
Block a user