/modtools/unban/", methods=["POST"])
+@rank_required(UserRank.MODERATOR)
+def modtools_unban(username):
+ user: User = User.query.filter_by(username=username).first()
+ if not user:
+ abort(404)
+
+ if not user.checkPerm(current_user, Permission.CHANGE_RANK):
+ abort(403)
+
+ if user.ban:
+ db.session.delete(user.ban)
+
+ if user.rank == UserRank.BANNED:
+ user.rank = UserRank.MEMBER
+
+ addAuditLog(AuditSeverity.MODERATION, current_user, f"Unbanned {user.username}",
+ url_for("users.profile", username=user.username), None)
+ db.session.commit()
+
+ flash(f"Unbanned {user.username}", "success")
+ return redirect(url_for("users.modtools", username=username))
diff --git a/app/models/users.py b/app/models/users.py
index 3af2feca..4df216d2 100644
--- a/app/models/users.py
+++ b/app/models/users.py
@@ -183,6 +183,8 @@ class User(db.Model, UserMixin):
replies = db.relationship("ThreadReply", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan", order_by=db.desc("created_at"))
forum_topics = db.relationship("ForumTopic", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
+ ban = db.relationship("UserBan", foreign_keys="UserBan.user_id", back_populates="user", uselist=False)
+
def __init__(self, username=None, active=False, email=None, password=None):
self.username = username
self.display_name = username
@@ -482,3 +484,21 @@ class UserNotificationPreferences(db.Model):
value = 1 if value else 0
setattr(self, "pref_" + notification_type.toName(), value)
+
+
+class UserBan(db.Model):
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
+ user = db.relationship("User", foreign_keys=[user_id], back_populates="ban")
+
+ message = db.Column(db.UnicodeText, nullable=False)
+
+ banned_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+ banned_by = db.relationship("User", foreign_keys=[banned_by_id])
+
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
+
+ expires_at = db.Column(db.DateTime, nullable=True, default=None)
+
+ @property
+ def has_expired(self):
+ return self.expires_at and datetime.datetime.now() > self.expires_at
diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html
index e2d96c30..8da8e4bd 100644
--- a/app/templates/packages/view.html
+++ b/app/templates/packages/view.html
@@ -434,15 +434,18 @@
{% if package.type == package.type.MOD %}
{{ _("Compatible Games") }}
- {% for support in package.getSortedSupportedGames() %}
-
- {{ _("%(title)s by %(display_name)s",
- title=support.game.title, display_name=support.game.author.display_name) }}
-
- {% else %}
- {{ _("No specific game is required") }}
- {% endfor %}
+
+
{{ _("This is an experimental feature.") }}
{{ _("Supported games are determined by an algorithm, and may not be correct.") }}
diff --git a/app/templates/users/modtools.html b/app/templates/users/modtools.html
index 846dff10..4575b40d 100644
--- a/app/templates/users/modtools.html
+++ b/app/templates/users/modtools.html
@@ -41,13 +41,37 @@
{% if not user.rank.atLeast(current_user.rank) %}
{{ _("Ban") }}
- {% if user.rank.name == "BANNED" %}
+ {% if user.ban %}
- Banned.
+ Banned by {{ user.ban.banned_by.display_name }} at {{ user.ban.created_at | full_datetime }}
+ {% if user.ban.expires_at %}
+ until {{ user.ban.expires_at | date }}
+ {% endif %}
+
+ {{ user.ban.message }}
+
+
{% else %}
{% endif %}
diff --git a/migrations/versions/01f8d5de29e1_.py b/migrations/versions/01f8d5de29e1_.py
new file mode 100644
index 00000000..797631c7
--- /dev/null
+++ b/migrations/versions/01f8d5de29e1_.py
@@ -0,0 +1,33 @@
+"""empty message
+
+Revision ID: 01f8d5de29e1
+Revises: e571b3498f9e
+Create Date: 2022-02-13 10:12:20.150232
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '01f8d5de29e1'
+down_revision = 'e571b3498f9e'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.create_table('user_ban',
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.Column('message', sa.UnicodeText(), nullable=False),
+ sa.Column('banned_by_id', sa.Integer(), nullable=False),
+ sa.Column('created_at', sa.DateTime(), nullable=False),
+ sa.Column('expires_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['banned_by_id'], ['user.id'], ),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+ sa.PrimaryKeyConstraint('user_id')
+ )
+
+
+def downgrade():
+ op.drop_table('user_ban')