From 46900390a53db581d15eff45ad3499dfaaf76ae7 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Wed, 13 Dec 2023 21:04:11 +1300 Subject: [PATCH] report posts and communities. also block domains and instances --- app/community/forms.py | 16 +++- app/community/routes.py | 47 +++++++++-- app/models.py | 35 ++++++++ app/post/forms.py | 31 ++++++- app/post/routes.py | 69 +++++++++++++++- app/static/scss/_typography.scss | 4 + app/static/structure.css | 12 +++ app/static/structure.scss | 8 ++ app/static/styles.css | 13 +++ app/static/styles.scss | 8 ++ app/templates/post/_post_full.html | 3 +- app/templates/post/_post_teaser.html | 2 +- app/templates/post/post_options.html | 20 ++++- app/templates/post/post_report.html | 17 ++++ app/utils.py | 8 +- .../versions/31dfc1d1d3f6_report_block.py | 82 +++++++++++++++++++ .../5fb8f21295da_community_instance.py | 36 ++++++++ 17 files changed, 393 insertions(+), 18 deletions(-) create mode 100644 app/templates/post/post_report.html create mode 100644 migrations/versions/31dfc1d1d3f6_report_block.py create mode 100644 migrations/versions/5fb8f21295da_community_instance.py diff --git a/app/community/forms.py b/app/community/forms.py index 6041c4b2..90db83ae 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -3,7 +3,7 @@ from wtforms import StringField, SubmitField, TextAreaField, BooleanField, Hidde from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional from flask_babel import _, lazy_gettext as _l -from app.utils import domain_from_url +from app.utils import domain_from_url, MultiCheckboxField class AddLocalCommunity(FlaskForm): @@ -83,3 +83,17 @@ class CreatePostForm(FlaskForm): return True + +class ReportCommunityForm(FlaskForm): + reason_choices = [('1', _l('Breaks instance rules')), + ('2', _l('Abandoned by moderators')), + ('3', _l('Cult')), + ('4', _l('Scam')), + ('5', _l('Alt-right pipeline')), + ('6', _l('Hate / genocide')), + ('7', _l('Other')), + ] + reasons = MultiCheckboxField(_l('Reason'), choices=reason_choices) + description = StringField(_l('More info')) + report_remote = BooleanField('Also send report to originating instance') + submit = SubmitField(_l('Report')) diff --git a/app/community/routes.py b/app/community/routes.py index e8cb894e..f034baae 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -1,24 +1,22 @@ from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort from flask_login import login_user, logout_user, current_user, login_required from flask_babel import _ -from pillow_heif import register_heif_opener from sqlalchemy import or_, desc from app import db, constants, cache from app.activitypub.signature import RsaKeys, HttpSignature from app.activitypub.util import default_context -from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm +from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm, ReportCommunityForm from app.community.util import search_for_community, community_url_exists, actor_to_community, \ ensure_directory_exists, opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \ SUBSCRIPTION_PENDING from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \ - File, PostVote, utcnow + File, PostVote, utcnow, Report, Notification, InstanceBlock from app.community import bp from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \ shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \ request_etag_matches, return_304 -import os from feedgen.feed import FeedGenerator from datetime import timezone @@ -38,7 +36,7 @@ def add_local(): community = Community(title=form.community_name.data, name=form.url.data, description=form.description.data, rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key, public_key=public_key, - ap_profile_id=current_app.config['SERVER_NAME'] + '/c/' + form.url.data, + ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data, subscriptions_count=1) icon_file = request.files['icon_file'] if icon_file and icon_file.filename != '': @@ -377,4 +375,43 @@ def add_post(actor): images_disabled=images_disabled) +@login_required +@bp.route('/community//report', methods=['GET', 'POST']) +def community_report(community_id: int): + community = Community.query.get_or_404(community_id) + form = ReportCommunityForm() + if form.validate_on_submit(): + report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data, + type=1, reporter_id=current_user.id, suspect_community_id=community.id) + db.session.add(report) + # Notify admin + # todo: find all instance admin(s). for now just load User.id == 1 + admins = [User.query.get_or_404(1)] + for admin in admins: + notification = Notification(user_id=admin.id, title=_('A post has been reported'), + url=community.local_url(), + author_id=current_user.id) + db.session.add(notification) + db.session.commit() + + # todo: federate report to originating instance + if not community.is_local() and form.report_remote.data: + ... + + flash(_('Community has been reported, thank you!')) + return redirect(community.local_url()) + + return render_template('community/community_report.html', title=_('Report community'), form=form, community=community) + + +@login_required +@bp.route('/community//block_instance', methods=['GET', 'POST']) +def community_block_instance(community_id: int): + community = Community.query.get_or_404(community_id) + existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=community.instance_id).first() + if not existing: + db.session.add(InstanceBlock(user_id=current_user.id, instance_id=community.instance_id)) + db.session.commit() + flash(_('Content from %(name)s will be hidden.', name=community.instance.domain)) + return redirect(community.local_url()) diff --git a/app/models.py b/app/models.py index bc50c021..f13b559e 100644 --- a/app/models.py +++ b/app/models.py @@ -70,11 +70,14 @@ class Community(db.Model): title = db.Column(db.String(256)) description = db.Column(db.Text) rules = db.Column(db.Text) + content_warning = db.Column(db.Text) # "Are you sure you want to view this community?" subscriptions_count = db.Column(db.Integer, default=0) post_count = db.Column(db.Integer, default=0) post_reply_count = db.Column(db.Integer, default=0) nsfw = db.Column(db.Boolean, default=False) nsfl = db.Column(db.Boolean, default=False) + instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True) + low_quality = db.Column(db.Boolean, default=False) # upvotes earned in low quality communities don't improve reputation created_at = db.Column(db.DateTime, default=utcnow) last_active = db.Column(db.DateTime, default=utcnow) public_key = db.Column(db.Text) @@ -94,6 +97,7 @@ class Community(db.Model): banned = db.Column(db.Boolean, default=False) restricted_to_mods = db.Column(db.Boolean, default=False) + new_mods_wanted = db.Column(db.Boolean, default=False) searchable = db.Column(db.Boolean, default=True) private_mods = db.Column(db.Boolean, default=False) @@ -175,6 +179,12 @@ class Community(db.Model): def is_local(self): return self.ap_id is None or self.profile_id().startswith('https://' + current_app.config['SERVER_NAME']) + def local_url(self): + if self.is_local(): + return self.ap_profile_id + else: + return f"https://{current_app.config['SERVER_NAME']}/c/{self.ap_id}" + user_role = db.Table('user_role', db.Column('user_id', db.Integer, db.ForeignKey('user.id')), @@ -207,6 +217,7 @@ class User(UserMixin, db.Model): bounces = db.Column(db.SmallInteger, default=0) timezone = db.Column(db.String(20)) reputation = db.Column(db.Float, default=0.0) + attitude = db.Column(db.Float, default=1.0) # (upvotes cast - downvotes cast) / (upvotes + downvotes). A number between 1 and -1 is the ratio between up and down votes they cast stripe_customer_id = db.Column(db.String(50)) stripe_subscription_id = db.Column(db.String(50)) searchable = db.Column(db.Boolean, default=True) @@ -410,6 +421,7 @@ class Post(db.Model): community_id = db.Column(db.Integer, db.ForeignKey('community.id'), index=True) image_id = db.Column(db.Integer, db.ForeignKey('file.id'), index=True) domain_id = db.Column(db.Integer, db.ForeignKey('domain.id'), index=True) + instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True) slug = db.Column(db.String(255)) title = db.Column(db.String(255)) url = db.Column(db.String(2048)) @@ -623,6 +635,15 @@ class Instance(db.Model): created_at = db.Column(db.DateTime, default=utcnow) updated_at = db.Column(db.DateTime, default=utcnow) + posts = db.relationship('Post', backref='instance', lazy='dynamic') + communities = db.relationship('Community', backref='instance', lazy='dynamic') + + +class InstanceBlock(db.Model): + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) + instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), primary_key=True) + created_at = db.Column(db.DateTime, default=utcnow) + class Settings(db.Model): name = db.Column(db.String(50), primary_key=True) @@ -716,6 +737,20 @@ class Notification(db.Model): created_at = db.Column(db.DateTime, default=utcnow) +class Report(db.Model): + id = db.Column(db.Integer, primary_key=True) + reasons = db.Column(db.String(256)) + description = db.Column(db.String(256)) + status = db.Column(db.Integer, default=0) + type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community + reporter_id = db.Column(db.Integer, db.ForeignKey('user.id')) + suspect_community_id = db.Column(db.Integer, db.ForeignKey('user.id')) + suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id')) + suspect_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id')) + created_at = db.Column(db.DateTime, default=utcnow) + updated = db.Column(db.DateTime, default=utcnow) + @login.user_loader def load_user(id): return User.query.get(int(id)) diff --git a/app/post/forms.py b/app/post/forms.py index 1a21decb..43668713 100644 --- a/app/post/forms.py +++ b/app/post/forms.py @@ -1,10 +1,37 @@ from flask_wtf import FlaskForm -from wtforms import TextAreaField, SubmitField, BooleanField +from wtforms import TextAreaField, SubmitField, BooleanField, StringField from wtforms.validators import DataRequired, Length from flask_babel import _, lazy_gettext as _l +from app.utils import MultiCheckboxField + class NewReplyForm(FlaskForm): body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 3}, validators={DataRequired(), Length(min=3, max=5000)}) notify_author = BooleanField(_l('Notify about replies')) - submit = SubmitField(_l('Comment')) \ No newline at end of file + submit = SubmitField(_l('Comment')) + + + +class ReportPostForm(FlaskForm): + reason_choices = [('1', _l('Breaks community rules')), ('7', _l('Spam')), ('2', _l('Harassment')), + ('3', _l('Threatening violence')), ('4', _l('Hate / genocide')), + ('6', _l('Sharing personal information')), + ('5', _l('Minor abuse or sexualization')), + ('8', _l('Non-consensual intimate media')), + ('9', _l('Prohibited transaction')), ('10', _l('Impersonation')), + ('11', _l('Copyright violation')), ('12', _l('Trademark violation')), + ('13', _l('Self-harm or suicide')), + ('14', _l('Other'))] + reasons = MultiCheckboxField(_l('Reason'), choices=reason_choices) + description = StringField(_l('More info')) + report_remote = BooleanField('Also send report to originating instance') + submit = SubmitField(_l('Report')) + + def reasons_to_string(self, reason_data) -> str: + result = [] + for reason_id in reason_data: + for choice in self.reason_choices: + if choice[0] == reason_id: + result.append(str(choice[1])) + return ', '.join(result) diff --git a/app/post/routes.py b/app/post/routes.py index 6853832c..62f732a5 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -9,12 +9,12 @@ from app import db, constants from app.activitypub.signature import HttpSignature from app.activitypub.util import default_context from app.community.util import save_post -from app.post.forms import NewReplyForm +from app.post.forms import NewReplyForm, ReportPostForm from app.community.forms import CreatePostForm from app.post.util import post_replies, get_comment_branch, post_reply_count from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE from app.models import Post, PostReply, \ - PostReplyVote, PostVote, Notification, utcnow + PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report from app.post import bp from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \ shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, ap_datetime, return_304, \ @@ -407,12 +407,73 @@ def post_delete(post_id: int): post.flush_cache() db.session.delete(post) db.session.commit() - flash('Post deleted.') + flash(_('Post deleted.')) return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name)) @login_required @bp.route('/post//report', methods=['GET', 'POST']) def post_report(post_id: int): - ... + post = Post.query.get_or_404(post_id) + form = ReportPostForm() + if form.validate_on_submit(): + report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data, + type=1, reporter_id=current_user.id, suspect_post_id=post.id) + db.session.add(report) + # Notify moderators + for mod in post.community.moderators(): + notification = Notification(user_id=mod.user_id, title=_('A post has been reported'), + url=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}", + author_id=current_user.id) + db.session.add(notification) + # todo: Also notify admins for certain types of report + db.session.commit() + + # todo: federate report to originating instance + if not post.community.is_local() and form.report_remote.data: + ... + + flash(_('Post has been reported, thank you!')) + return redirect(post.community.local_url()) + + return render_template('post/post_report.html', title=_('Report post'), form=form, post=post) + + +@login_required +@bp.route('/post//block_user', methods=['GET', 'POST']) +def post_block_user(post_id: int): + post = Post.query.get_or_404(post_id) + existing = UserBlock.query.filter_by(blocker_id=current_user.id, blocked_id=post.author.id).first() + if not existing: + db.session.add(UserBlock(blocker_id=current_user.id, blocked_id=post.author.id)) + db.session.commit() + flash(_('%(name)s has been blocked.', name=post.author.user_name)) + + # todo: federate block to post author instance + + return redirect(post.community.local_url()) + + +@login_required +@bp.route('/post//block_domain', methods=['GET', 'POST']) +def post_block_domain(post_id: int): + post = Post.query.get_or_404(post_id) + existing = DomainBlock.query.filter_by(user_id=current_user.id, domain_id=post.domain_id).first() + if not existing: + db.session.add(DomainBlock(user_id=current_user.id, domain_id=post.domain_id)) + db.session.commit() + flash(_('Posts linking to %(name)s will be hidden.', name=post.domain.name)) + return redirect(post.community.local_url()) + + +@login_required +@bp.route('/post//block_instance', methods=['GET', 'POST']) +def post_block_instance(post_id: int): + post = Post.query.get_or_404(post_id) + existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=post.instance_id).first() + if not existing: + db.session.add(InstanceBlock(user_id=current_user.id, instance_id=post.instance_id)) + db.session.commit() + flash(_('Content from %(name)s will be hidden.', name=post.instance.domain)) + return redirect(post.community.local_url()) diff --git a/app/static/scss/_typography.scss b/app/static/scss/_typography.scss index 359201c0..574c5783 100644 --- a/app/static/scss/_typography.scss +++ b/app/static/scss/_typography.scss @@ -178,6 +178,10 @@ content: "\ea03"; } +.fe-block::before { + content: "\ea04"; +} + .fe-report::before { content: "\e967"; } diff --git a/app/static/structure.css b/app/static/structure.css index dace866b..c6cec898 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -181,6 +181,10 @@ nav, etc which are used site-wide */ content: "\ea03"; } +.fe-block::before { + content: "\ea04"; +} + .fe-report::before { content: "\e967"; } @@ -579,6 +583,14 @@ fieldset legend { padding-left: 3px; } +#reasons { + border: none; + list-style-type: none; + padding: 0; + overflow-y: auto; + height: 135px; +} + .table tr th { vertical-align: middle; } diff --git a/app/static/structure.scss b/app/static/structure.scss index 33b6653f..573d5915 100644 --- a/app/static/structure.scss +++ b/app/static/structure.scss @@ -325,6 +325,14 @@ nav, etc which are used site-wide */ } } +#reasons { + border: none; + list-style-type: none; + padding: 0; + overflow-y: auto; + height: 135px; +} + .table { tr th { vertical-align: middle; diff --git a/app/static/styles.css b/app/static/styles.css index 9355f73b..502af400 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -180,6 +180,10 @@ content: "\ea03"; } +.fe-block::before { + content: "\ea04"; +} + .fe-report::before { content: "\e967"; } @@ -480,6 +484,15 @@ nav.navbar { margin-bottom: 10px; } +.post_options_link { + display: block; + position: absolute; + bottom: 0; + right: -2px; + width: 41px; + text-decoration: none; +} + @media (prefers-color-scheme: dark) { body { background-color: #777; diff --git a/app/static/styles.scss b/app/static/styles.scss index 53c3c585..f12cfa89 100644 --- a/app/static/styles.scss +++ b/app/static/styles.scss @@ -197,6 +197,14 @@ nav.navbar { margin-bottom: 10px; } +.post_options_link { + display: block; + position: absolute; + bottom: 0; + right: -2px; + width: 41px; + text-decoration: none; +} @media (prefers-color-scheme: dark) { body { background-color: $dark-grey; diff --git a/app/templates/post/_post_full.html b/app/templates/post/_post_full.html index 08e131eb..856fae1f 100644 --- a/app/templates/post/_post_full.html +++ b/app/templates/post/_post_full.html @@ -1,4 +1,4 @@ -
+
{% if post.type == POST_TYPE_IMAGE %}
{% endif %} +
{% if post.body_html %} diff --git a/app/templates/post/_post_teaser.html b/app/templates/post/_post_teaser.html index b3067a34..97500d34 100644 --- a/app/templates/post/_post_teaser.html +++ b/app/templates/post/_post_teaser.html @@ -31,7 +31,7 @@ {{ post.reply_count }}
- +
diff --git a/app/templates/post/post_options.html b/app/templates/post/post_options.html index 25f1d8ec..3b13255d 100644 --- a/app/templates/post/post_options.html +++ b/app/templates/post/post_options.html @@ -9,11 +9,25 @@
{{ _('Options for "%(post_title)s"', post_title=post.title) }}
diff --git a/app/templates/post/post_report.html b/app/templates/post/post_report.html new file mode 100644 index 00000000..c706129b --- /dev/null +++ b/app/templates/post/post_report.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% from 'bootstrap/form.html' import render_form %} + +{% block app_content %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/app/utils.py b/app/utils.py index 7bb08058..14a67b31 100644 --- a/app/utils.py +++ b/app/utils.py @@ -14,7 +14,8 @@ import imghdr from flask import current_app, json, redirect, url_for, request, make_response, Response from flask_login import current_user from sqlalchemy import text - +from wtforms.fields import SelectField, SelectMultipleField +from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput from app import db, cache from app.models import Settings, Domain, Instance, BannedInstances, User, Community @@ -293,3 +294,8 @@ def back(default_url): # format a datetime in a way that is used in ActivityPub def ap_datetime(date_time: datetime) -> str: return date_time.isoformat() + '+00:00' + + +class MultiCheckboxField(SelectMultipleField): + widget = ListWidget(prefix_label=False) + option_widget = CheckboxInput() \ No newline at end of file diff --git a/migrations/versions/31dfc1d1d3f6_report_block.py b/migrations/versions/31dfc1d1d3f6_report_block.py new file mode 100644 index 00000000..3bfdf4f4 --- /dev/null +++ b/migrations/versions/31dfc1d1d3f6_report_block.py @@ -0,0 +1,82 @@ +"""report_block + +Revision ID: 31dfc1d1d3f6 +Revises: b36dac7696d1 +Create Date: 2023-12-13 19:11:27.447598 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '31dfc1d1d3f6' +down_revision = 'b36dac7696d1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('instance_block', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('instance_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['instance_id'], ['instance.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'instance_id') + ) + op.create_table('report', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('reasons', sa.String(length=256), nullable=True), + sa.Column('description', sa.String(length=256), nullable=True), + sa.Column('status', sa.Integer(), nullable=True), + sa.Column('type', sa.Integer(), nullable=True), + sa.Column('reporter_id', sa.Integer(), nullable=True), + sa.Column('suspect_community_id', sa.Integer(), nullable=True), + sa.Column('suspect_user_id', sa.Integer(), nullable=True), + sa.Column('suspect_post_id', sa.Integer(), nullable=True), + sa.Column('suspect_reply_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['reporter_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['suspect_community_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['suspect_post_id'], ['post.id'], ), + sa.ForeignKeyConstraint(['suspect_reply_id'], ['post_reply.id'], ), + sa.ForeignKeyConstraint(['suspect_user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.add_column(sa.Column('content_warning', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('low_quality', sa.Boolean(), nullable=True)) + batch_op.add_column(sa.Column('new_mods_wanted', sa.Boolean(), nullable=True)) + + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.add_column(sa.Column('instance_id', sa.Integer(), nullable=True)) + batch_op.create_index(batch_op.f('ix_post_instance_id'), ['instance_id'], unique=False) + batch_op.create_foreign_key(None, 'instance', ['instance_id'], ['id']) + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('attitude', sa.Float(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_column('attitude') + + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_index(batch_op.f('ix_post_instance_id')) + batch_op.drop_column('instance_id') + + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.drop_column('new_mods_wanted') + batch_op.drop_column('low_quality') + batch_op.drop_column('content_warning') + + op.drop_table('report') + op.drop_table('instance_block') + # ### end Alembic commands ### diff --git a/migrations/versions/5fb8f21295da_community_instance.py b/migrations/versions/5fb8f21295da_community_instance.py new file mode 100644 index 00000000..6dd23801 --- /dev/null +++ b/migrations/versions/5fb8f21295da_community_instance.py @@ -0,0 +1,36 @@ +"""community instance + +Revision ID: 5fb8f21295da +Revises: 31dfc1d1d3f6 +Create Date: 2023-12-13 20:57:09.647260 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5fb8f21295da' +down_revision = '31dfc1d1d3f6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.add_column(sa.Column('instance_id', sa.Integer(), nullable=True)) + batch_op.create_index(batch_op.f('ix_community_instance_id'), ['instance_id'], unique=False) + batch_op.create_foreign_key(None, 'instance', ['instance_id'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_index(batch_op.f('ix_community_instance_id')) + batch_op.drop_column('instance_id') + + # ### end Alembic commands ###