mirror of
https://github.com/minetest/contentdb.git
synced 2025-01-03 03:37:28 +01:00
Add user deletion / deactivation
This commit is contained in:
parent
fd0b203f1e
commit
778a602aa6
@ -137,7 +137,6 @@ def view(package):
|
|||||||
.all()
|
.all()
|
||||||
|
|
||||||
releases = getReleases(package)
|
releases = getReleases(package)
|
||||||
requests = [r for r in package.requests if r.status == 0]
|
|
||||||
|
|
||||||
review_thread = package.review_thread
|
review_thread = package.review_thread
|
||||||
if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_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
|
has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0
|
||||||
|
|
||||||
return render_template("packages/view.html",
|
return render_template("packages/view.html",
|
||||||
package=package, releases=releases, requests=requests,
|
package=package, releases=releases,
|
||||||
alternatives=alternatives, similar_topics=similar_topics,
|
alternatives=alternatives, similar_topics=similar_topics,
|
||||||
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl,
|
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl,
|
||||||
threads=threads.all(), has_review=has_review)
|
threads=threads.all(), has_review=has_review)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from flask import *
|
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 flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
|
|
||||||
from app.models 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 app.tasks.emails import send_verify_email
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
@ -186,3 +186,38 @@ def email_notifications(username=None):
|
|||||||
return render_template("users/settings_email.html",
|
return render_template("users/settings_email.html",
|
||||||
form=form, user=user, types=types, is_new=is_new,
|
form=form, user=user, types=types, is_new=is_new,
|
||||||
tabs=get_setting_tabs(user), current_tab="notifications")
|
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)
|
donate_url = db.Column(db.String(255), nullable=True, default=None)
|
||||||
|
|
||||||
# Content
|
# Content
|
||||||
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id",
|
notifications = db.relationship("Notification", foreign_keys="Notification.user_id",
|
||||||
order_by=desc(text("Notification.created_at")))
|
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")
|
packages = db.relationship("Package", back_populates="author", lazy="dynamic")
|
||||||
reviews = db.relationship("PackageReview", back_populates="author", order_by=db.desc("review_created_at"))
|
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")
|
tokens = db.relationship("APIToken", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||||
threads = db.relationship("Thread", back_populates="author", lazy="dynamic")
|
threads = db.relationship("Thread", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||||
replies = db.relationship("ThreadReply", back_populates="author", lazy="dynamic")
|
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):
|
def __init__(self, username=None, active=False, email=None, password=None):
|
||||||
self.username = username
|
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_EMAIL) or \
|
||||||
self.checkPerm(current_user, Permission.CHANGE_RANK)
|
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):
|
class UserEmailVerification(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@ -367,10 +374,10 @@ class Notification(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
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_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)
|
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)
|
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_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
|
# Downloads
|
||||||
repo = db.Column(db.String(200), nullable=True)
|
repo = db.Column(db.String(200), nullable=True)
|
||||||
@ -734,7 +741,7 @@ class Package(db.Model):
|
|||||||
maintainers = db.relationship("User", secondary=maintainers, lazy="subquery")
|
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")
|
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):
|
def __init__(self, package=None):
|
||||||
if package is None:
|
if package is None:
|
||||||
@ -1337,10 +1344,12 @@ class Thread(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=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_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_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
author = db.relationship("User", back_populates="threads", foreign_keys=[author_id])
|
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)
|
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):
|
def asSign(self):
|
||||||
return 1 if self.recommends else -1
|
return 1 if self.recommends else -1
|
||||||
@ -1473,14 +1482,13 @@ class AuditSeverity(enum.Enum):
|
|||||||
return item if type(item) == AuditSeverity else AuditSeverity[item]
|
return item if type(item) == AuditSeverity else AuditSeverity[item]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AuditLogEntry(db.Model):
|
class AuditLogEntry(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
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_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
|
||||||
causer = db.relationship("User", foreign_keys=[causer_id])
|
causer = db.relationship("User", back_populates="", foreign_keys=[causer_id])
|
||||||
|
|
||||||
severity = db.Column(db.Enum(AuditSeverity), nullable=False)
|
severity = db.Column(db.Enum(AuditSeverity), nullable=False)
|
||||||
|
|
||||||
|
@ -32,12 +32,16 @@ Audit Log
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-2 text-muted">
|
<div class="col-sm-2 text-muted">
|
||||||
<img
|
{% if entry.causer %}
|
||||||
class="img-fluid user-photo img-thumbnail img-thumbnail-1"
|
<img
|
||||||
style="max-height: 22px;"
|
class="img-fluid user-photo img-thumbnail img-thumbnail-1"
|
||||||
src="{{ entry.causer.getProfilePicURL() }}" />
|
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>
|
||||||
|
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
|
@ -10,9 +10,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h1>{{ entry.title }}</h1>
|
<h1>{{ entry.title }}</h1>
|
||||||
<p class="text-muted mb-4">
|
|
||||||
{{ _("Caused by %(author)s.", author=entry.causer.display_name) }}
|
{% if entry.causer %}
|
||||||
</p>
|
<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>
|
<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