soft deletes and deleted content admin area #193

This commit is contained in:
rimu 2024-06-02 16:45:21 +12:00
parent f4931b474c
commit 2380a3ae61
19 changed files with 326 additions and 107 deletions

View file

@ -1010,7 +1010,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
post.delete_dependencies()
post.community.post_count -= 1
announce_activity_to_followers(post.community, post.author, request_json)
db.session.delete(post)
post.deleted = True
db.session.commit()
activity_log.result = 'success'
else:
@ -1023,6 +1023,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
reply.body_html = '<p><em>deleted</em></p>'
reply.body = 'deleted'
reply.post.reply_count -= 1
reply.deleted = True
announce_activity_to_followers(reply.community, reply.author, request_json)
db.session.commit()
activity_log.result = 'success'
@ -1213,7 +1214,7 @@ def community_outbox(actor):
actor = actor.strip()
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
if community is not None:
posts = community.posts.limit(50).all()
posts = community.posts.filter(Post.deleted == False).limit(50).all()
community_data = {
"@context": default_context(),
@ -1234,7 +1235,7 @@ def community_featured(actor):
actor = actor.strip()
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
if community is not None:
posts = Post.query.filter_by(community_id=community.id, sticky=True).all()
posts = Post.query.filter_by(community_id=community.id, sticky=True, deleted=False).all()
community_data = {
"@context": default_context(),

View file

@ -78,11 +78,11 @@ def active_day():
def local_posts():
return db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE instance_id = 1')).scalar()
return db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE instance_id = 1 AND deleted is false')).scalar()
def local_comments():
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE instance_id = 1')).scalar()
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE instance_id = 1 and deleted is false')).scalar()
def local_communities():
@ -1409,7 +1409,7 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id
if deletor.is_admin() or community.is_moderator(deletor) or community.is_instance_admin(deletor) or to_delete.author.id == deletor.id:
if isinstance(to_delete, Post):
to_delete.delete_dependencies()
db.session.delete(to_delete)
to_delete.deleted = True
community.post_count -= 1
db.session.commit()
elif isinstance(to_delete, PostReply):
@ -1419,7 +1419,7 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id
to_delete.body_html = lemmy_markdown_to_html(to_delete.body)
else:
to_delete.delete_dependencies()
db.session.delete(to_delete)
to_delete.deleted = True
db.session.commit()
@ -1447,31 +1447,30 @@ def remove_data_from_banned_user_task(deletor_ap_id, user_ap_id, target):
# community bans by mods
elif community and community.is_moderator(deletor):
post_replies = PostReply.query.filter_by(user_id=user.id, community_id=community.id)
posts = Post.query.filter_by(user_id=user.id, community_id=community.id)
post_replies = PostReply.query.filter_by(user_id=user.id, community_id=community.id, deleted=False)
posts = Post.query.filter_by(user_id=user.id, community_id=community.id, deleted=False)
else:
return
for pr in post_replies:
pr.post.reply_count -= 1
if pr.has_replies():
pr.body = 'Banned'
pr.body_html = lemmy_markdown_to_html(pr.body)
for post_reply in post_replies:
post_reply.post.reply_count -= 1
if post_reply.has_replies():
post_reply.body = 'Banned'
post_reply.body_html = lemmy_markdown_to_html(post_reply.body)
else:
pr.delete_dependencies()
db.session.delete(pr)
post_reply.delete_dependencies()
post_reply.deleted = True
db.session.commit()
for p in posts:
if p.cross_posts:
old_cross_posts = Post.query.filter(Post.id.in_(p.cross_posts)).all()
for post in posts:
if post.cross_posts:
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
for ocp in old_cross_posts:
if ocp.cross_posts is not None:
ocp.cross_posts.remove(p.id)
p.delete_dependencies()
db.session.delete(p)
p.community.post_count -= 1
ocp.cross_posts.remove(post.id)
post.delete_dependencies()
post.deleted = True
post.community.post_count -= 1
db.session.commit()

View file

@ -552,7 +552,7 @@ def admin_content_trash():
page = request.args.get('page', 1, type=int)
posts = Post.query.filter(Post.posted_at > utcnow() - timedelta(days=3)).order_by(Post.score)
posts = Post.query.filter(Post.posted_at > utcnow() - timedelta(days=3), Post.deleted == False).order_by(Post.score)
posts = posts.paginate(page=page, per_page=100, error_out=False)
next_url = url_for('admin.admin_content_trash', page=posts.next_num) if posts.has_next else None
@ -577,12 +577,14 @@ def admin_content_spam():
posts = Post.query.join(User, User.id == Post.user_id).\
filter(User.created > utcnow() - timedelta(days=3)).\
filter(Post.posted_at > utcnow() - timedelta(days=3)).\
filter(Post.deleted == False).\
filter(Post.score <= 0).order_by(Post.score)
posts = posts.paginate(page=page, per_page=100, error_out=False)
post_replies = PostReply.query.join(User, User.id == PostReply.user_id). \
filter(User.created > utcnow() - timedelta(days=3)). \
filter(PostReply.posted_at > utcnow() - timedelta(days=3)). \
filter(PostReply.deleted == False). \
filter(PostReply.score <= 0).order_by(PostReply.score)
post_replies = post_replies.paginate(page=replies_page, per_page=100, error_out=False)
@ -602,6 +604,40 @@ def admin_content_spam():
)
@bp.route('/content/deleted', methods=['GET'])
@login_required
@permission_required('administer all users')
def admin_content_deleted():
# Shows all soft deleted posts
page = request.args.get('page', 1, type=int)
replies_page = request.args.get('replies_page', 1, type=int)
posts = Post.query.\
filter(Post.deleted == True).\
order_by(Post.posted_at)
posts = posts.paginate(page=page, per_page=100, error_out=False)
post_replies = PostReply.query. \
filter(PostReply.deleted == True). \
order_by(PostReply.posted_at)
post_replies = post_replies.paginate(page=replies_page, per_page=100, error_out=False)
next_url = url_for('admin.admin_content_deleted', page=posts.next_num) if posts.has_next else None
prev_url = url_for('admin.admin_content_deleted', page=posts.prev_num) if posts.has_prev and page != 1 else None
next_url_replies = url_for('admin.admin_content_deleted', replies_page=post_replies.next_num) if post_replies.has_next else None
prev_url_replies = url_for('admin.admin_content_deleted', replies_page=post_replies.prev_num) if post_replies.has_prev and replies_page != 1 else None
return render_template('admin/deleted_posts.html', title=_('Deleted content'),
next_url=next_url, prev_url=prev_url,
next_url_replies=next_url_replies, prev_url_replies=prev_url_replies,
posts=posts, post_replies=post_replies,
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()),
menu_topics=menu_topics(),
site=g.site
)
@bp.route('/approve_registrations', methods=['GET'])
@login_required
@permission_required('approve registrations')

View file

@ -266,6 +266,15 @@ def register(app):
InstanceRole.role == 'admin').delete()
db.session.commit()
# Delete soft-deleted content after 7 days
for post_reply in PostReply.query.filter(PostReply.deleted == True, PostReply.posted_at < utcnow() - timedelta(days=7)).all():
db.session.delete(post_reply)
for post in Post.query.filter(Post.deleted == True, Post.posted_at < utcnow() - timedelta(days=7)).all():
db.session.delete(post)
db.session.commit()
@app.cli.command("spaceusage")
def spaceusage():
with app.app_context():

View file

@ -197,7 +197,7 @@ def show_community(community: Community):
# filter out nsfw and nsfl if desired
if current_user.is_anonymous:
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False, Post.deleted == False)
content_filters = {}
else:
if current_user.ignore_bots:
@ -207,6 +207,7 @@ def show_community(community: Community):
if current_user.show_nsfw is False:
posts = posts.filter(Post.nsfw == False)
content_filters = user_filters_posts(current_user.id)
posts = posts.filter(Post.deleted == False)
# filter domains and instances
domains_ids = blocked_domains(current_user.id)
@ -319,7 +320,7 @@ def show_community_rss(actor):
if request_etag_matches(current_etag):
return return_304(current_etag, 'application/rss+xml')
posts = community.posts.filter(Post.from_bot == False).order_by(desc(Post.created_at)).limit(100).all()
posts = community.posts.filter(Post.from_bot == False, Post.deleted == False).order_by(desc(Post.created_at)).limit(100).all()
description = shorten_string(community.description, 150) if community.description else None
og_image = community.image.source_url if community.image_id else None
fg = FeedGenerator()

View file

@ -541,7 +541,7 @@ def delete_post_from_community_task(post_id):
post = Post.query.get(post_id)
community = post.community
post.delete_dependencies()
db.session.delete(post)
post.deleted = True
db.session.commit()
if not community.local_only:
@ -600,7 +600,7 @@ def delete_post_reply_from_community_task(post_reply_id):
post_reply.body_html = markdown_to_html(post_reply.body)
else:
post_reply.delete_dependencies()
db.session.delete(post_reply)
post_reply.deleted = True
db.session.commit()
# federate delete

View file

@ -26,10 +26,10 @@ def show_domain(domain_id):
if domain:
if current_user.is_anonymous or current_user.ignore_bots:
posts = Post.query.join(Community, Community.id == Post.community_id).\
filter(Post.from_bot == False, Post.domain_id == domain.id, Community.banned == False).\
filter(Post.from_bot == False, Post.domain_id == domain.id, Community.banned == False, Post.deleted == False).\
order_by(desc(Post.posted_at))
else:
posts = Post.query.join(Community).filter(Post.domain_id == domain.id, Community.banned == False).order_by(desc(Post.posted_at))
posts = Post.query.join(Community).filter(Post.domain_id == domain.id, Community.banned == False, Post.deleted == False).order_by(desc(Post.posted_at))
if current_user.is_authenticated:
instance_ids = blocked_instances(current_user.id)

View file

@ -30,7 +30,7 @@ from app.utils import render_template, get_setting, gibberish, request_etag_matc
blocked_instances, communities_banned_from, topic_tree, recently_upvoted_posts, recently_downvoted_posts, \
generate_image_from_video_url, blocked_users, microblog_content_to_title, menu_topics
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \
InstanceRole, Notification, Language, community_language
InstanceRole, Notification, Language, community_language, PostReply
from PIL import Image
import pytesseract
@ -73,7 +73,7 @@ def home_page(type, sort):
if current_user.is_anonymous:
flash(_('Create an account to tailor this feed to your interests.'))
posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False, Post.deleted == False)
posts = posts.join(Community, Community.id == Post.community_id)
if type == 'home':
posts = posts.filter(Community.show_home == True)
@ -368,7 +368,7 @@ def robots():
@bp.route('/sitemap.xml')
@cache.cached(timeout=6000)
def sitemap():
posts = Post.query.filter(Post.from_bot == False)
posts = Post.query.filter(Post.from_bot == False, Post.deleted == False)
posts = posts.join(Community, Community.id == Post.community_id)
posts = posts.filter(Community.show_all == True, Community.ap_id == None) # sitemap.xml only includes local posts
if not g.site.enable_nsfw:
@ -396,6 +396,10 @@ def list_files(directory):
@bp.route('/test')
def test():
#for community in Community.query.filter(Community.content_retention != -1):
# for post in community.posts.filter(Post.posted_at < utcnow() - timedelta(days=Community.content_retention)):
# post.delete_dependencies()
return 'done'

View file

@ -880,7 +880,7 @@ class User(UserMixin, db.Model):
db.session.query(Notification).filter(Notification.user_id == self.id).delete()
db.session.query(PollChoiceVote).filter(PollChoiceVote.user_id == self.id).delete()
def purge_content(self):
def purge_content(self, soft=True):
files = File.query.join(Post).filter(Post.user_id == self.id).all()
for file in files:
file.delete_from_disk()
@ -888,12 +888,18 @@ class User(UserMixin, db.Model):
posts = Post.query.filter_by(user_id=self.id).all()
for post in posts:
post.delete_dependencies()
db.session.delete(post)
if soft:
post.deleted = True
else:
db.session.delete(post)
db.session.commit()
post_replies = PostReply.query.filter_by(user_id=self.id).all()
for reply in post_replies:
reply.delete_dependencies()
db.session.delete(reply)
if soft:
reply.deleted = True
else:
db.session.delete(reply)
db.session.commit()
def mention_tag(self):
@ -938,6 +944,7 @@ class Post(db.Model):
body_html = db.Column(db.Text)
type = db.Column(db.Integer)
comments_enabled = db.Column(db.Boolean, default=True)
deleted = db.Column(db.Boolean, default=False, index=True)
mea_culpa = db.Column(db.Boolean, default=False)
has_embed = db.Column(db.Boolean, default=False)
reply_count = db.Column(db.Integer, default=0)
@ -1066,6 +1073,7 @@ class PostReply(db.Model):
notify_author = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, index=True, default=utcnow)
posted_at = db.Column(db.DateTime, index=True, default=utcnow)
deleted = db.Column(db.Boolean, default=False, index=True)
ip = db.Column(db.String(50))
from_bot = db.Column(db.Boolean, default=False)
up_votes = db.Column(db.Integer, default=0)
@ -1142,7 +1150,7 @@ class PostReply(db.Model):
return PostReply.query.filter_by(parent_id=self.id).all()
def has_replies(self):
reply = PostReply.query.filter_by(parent_id=self.id).first()
reply = PostReply.query.filter_by(parent_id=self.id).filter(PostReply.deleted == False).first()
return reply is not None
def blocked_by_content_filter(self, content_filters):

View file

@ -36,7 +36,7 @@ def show_post(post_id: int):
post = Post.query.get_or_404(post_id)
community: Community = post.community
if community.banned:
if community.banned or post.deleted:
abort(404)
sort = request.args.get('sort', 'hot')
@ -231,6 +231,7 @@ def show_post(post_id: int):
og_image = post.image.source_url if post.image_id else None
description = shorten_string(markdown_to_text(post.body), 150) if post.body else None
# Breadcrumbs
breadcrumbs = []
breadcrumb = namedtuple("Breadcrumb", ['text', 'url'])
breadcrumb.text = _('Home')
@ -288,15 +289,15 @@ def show_post(post_id: int):
poll_total_votes = 0
if post.type == POST_TYPE_POLL:
poll_data = Poll.query.get(post.id)
poll_choices = PollChoice.query.filter_by(post_id=post.id).order_by(PollChoice.sort_order).all()
poll_total_votes = poll_data.total_votes()
# Show poll results to everyone after the poll finishes, to the poll creator and to those who have voted
if (current_user.is_authenticated and (poll_data.has_voted(current_user.id))) \
or poll_data.end_poll < datetime.utcnow():
poll_results = True
else:
poll_form = True
if poll_data:
poll_choices = PollChoice.query.filter_by(post_id=post.id).order_by(PollChoice.sort_order).all()
poll_total_votes = poll_data.total_votes()
# Show poll results to everyone after the poll finishes, to the poll creator and to those who have voted
if (current_user.is_authenticated and (poll_data.has_voted(current_user.id))) \
or poll_data.end_poll < datetime.utcnow():
poll_results = True
else:
poll_form = True
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list,
@ -612,7 +613,7 @@ def poll_vote(post_id):
def continue_discussion(post_id, comment_id):
post = Post.query.get_or_404(post_id)
comment = PostReply.query.get_or_404(comment_id)
if post.community.banned:
if post.community.banned or post.deleted or comment.deleted:
abort(404)
mods = post.community.moderators()
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
@ -844,6 +845,10 @@ def add_reply(post_id: int, comment_id: int):
@bp.route('/post/<int:post_id>/options', methods=['GET'])
def post_options(post_id: int):
post = Post.query.get_or_404(post_id)
if current_user.is_anonymous or not current_user.is_admin():
if post.deleted:
abort(404)
return render_template('post/post_options.html', post=post,
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()),
@ -854,6 +859,9 @@ def post_options(post_id: int):
def post_reply_options(post_id: int, comment_id: int):
post = Post.query.get_or_404(post_id)
post_reply = PostReply.query.get_or_404(comment_id)
if current_user.is_anonymous or not current_user.is_admin():
if post.deleted or post_reply.deleted:
abort(404)
return render_template('post/post_reply_options.html', post=post, post_reply=post_reply,
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()),
@ -1467,7 +1475,7 @@ def post_delete(post_id: int):
if ocp.cross_posts is not None:
ocp.cross_posts.remove(post.id)
post.delete_dependencies()
db.session.delete(post)
post.deleted = True
g.site.last_active = community.last_active = utcnow()
db.session.commit()
flash(_('Post deleted.'))
@ -1524,6 +1532,61 @@ def post_delete(post_id: int):
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))
@bp.route('/post/<int:post_id>/restore', methods=['GET', 'POST'])
@login_required
def post_restore(post_id: int):
post = Post.query.get_or_404(post_id)
if post.community.is_moderator() or post.community.is_owner() or current_user.is_admin():
post.deleted = False
db.session.commit()
# Federate un-delete
if post.is_local():
delete_json = {
"actor": current_user.profile_id(),
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": {
'id': f"https://{current_app.config['SERVER_NAME']}/activities/delete/{gibberish(15)}",
'type': 'Delete',
'actor': current_user.profile_id(),
'audience': post.community.profile_id(),
'to': [post.community.profile_id(), 'https://www.w3.org/ns/activitystreams#Public'],
'published': ap_datetime(utcnow()),
'cc': [
current_user.followers_url()
],
'object': post.ap_id,
'uri': post.ap_id,
"summary": "bad post",
},
"cc": [post.community.profile_id()],
"audience": post.author.profile_id(),
"type": "Undo",
"id": f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}"
}
announce = {
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
"type": 'Announce',
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"actor": post.community.ap_profile_id,
"cc": [
post.community.ap_followers_url
],
'@context': default_context(),
'object': delete_json
}
for instance in post.community.following_instances():
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
send_to_remote_instance(instance.id, post.community.id, announce)
flash(_('Post has been restored.'))
return redirect(url_for('activitypub.post_ap', post_id=post.id))
@bp.route('/post/<int:post_id>/report', methods=['GET', 'POST'])
@login_required
def post_report(post_id: int):
@ -1906,7 +1969,7 @@ def post_reply_delete(post_id: int, comment_id: int):
post_reply.body_html = markdown_to_html(post_reply.body)
else:
post_reply.delete_dependencies()
db.session.delete(post_reply)
post_reply.deleted = True
g.site.last_active = community.last_active = utcnow()
db.session.commit()
flash(_('Comment deleted.'))

View file

@ -10,7 +10,7 @@ from app.utils import blocked_instances, blocked_users
# replies to a post, in a tree, sorted by a variety of methods
def post_replies(post_id: int, sort_by: str, show_first: int = 0) -> List[PostReply]:
comments = PostReply.query.filter_by(post_id=post_id)
comments = PostReply.query.filter_by(post_id=post_id).filter(PostReply.deleted == False)
if current_user.is_authenticated:
instance_ids = blocked_instances(current_user.id)
if instance_ids:
@ -46,7 +46,7 @@ def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[Post
if parent_comment is None:
return []
comments = PostReply.query.filter(PostReply.post_id == post_id)
comments = PostReply.query.filter(PostReply.post_id == post_id, PostReply.deleted == False)
if current_user.is_authenticated:
instance_ids = blocked_instances(current_user.id)
if instance_ids:
@ -71,7 +71,7 @@ def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[Post
# The number of replies a post has
def post_reply_count(post_id) -> int:
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id'),
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id AND deleted is false'),
{'post_id': post_id}).scalar()

View file

@ -22,7 +22,7 @@ def show_tag(tag):
posts = Post.query.join(Community, Community.id == Post.community_id). \
join(post_tag, post_tag.c.post_id == Post.id).filter(post_tag.c.tag_id == tag.id). \
filter(Community.banned == False)
filter(Community.banned == False, Post.deleted == False)
if current_user.is_anonymous or current_user.ignore_bots:
posts = posts.filter(Post.from_bot == False)

View file

@ -0,0 +1,41 @@
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
{% extends 'themes/' + theme() + '/base.html' %}
{% else %}
{% extends "base.html" %}
{% endif %} %}
{% from 'bootstrap/form.html' import render_form %}
{% set active_child = 'admin_content_deleted' %}
{% block app_content %}
<div class="row">
<div class="col">
<h1>{{ _('Deleted posts') }}</h1>
<div class="post_list">
{% for post in posts.items %}
{% include 'post/_post_teaser.html' %}
{% else %}
<p>{{ _('No deleted posts.') }}</p>
{% endfor %}
</div>
{% if post_replies %}
<h2 class="mt-4" id="comments">Deleted comments</h2>
<div class="post_list">
{% for post_reply in post_replies.items %}
{% include 'post/_post_reply_teaser.html' %}
{% else %}
<p>{{ _('No deleted comments.') }}</p>
{% endfor %}
</div>
{% else %}
<p>{{ _('No comments yet.') }}</p>
{% endif %}
</div>
</div>
<hr />
<div class="row">
<div class="col">
{% include 'admin/_nav.html' %}
</div>
</div>
<hr />
{% endblock %}

View file

@ -213,6 +213,7 @@
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_users_trash' }}" href="{{ url_for('admin.admin_users_trash', local_remote='local') }}">{{ _('Monitoring - users') }}</a></li>
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_content_trash' }}" href="{{ url_for('admin.admin_content_trash') }}">{{ _('Monitoring - content') }}</a></li>
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_content_spam' }}" href="{{ url_for('admin.admin_content_spam') }}">{{ _('Monitoring - spammy content') }}</a></li>
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_content_deleted' }}" href="{{ url_for('admin.admin_content_deleted') }}">{{ _('Deleted content') }}</a></li>
{% if g.site.registration_mode == 'RequireApplication' %}
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_approve_registrations' }}" href="{{ url_for('admin.admin_approve_registrations') }}">{{ _('Registration applications') }}</a></li>
{% endif %}

View file

@ -1,51 +1,56 @@
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
{% extends 'themes/' + theme() + '/base.html' %}
{% else %}
{% extends "base.html" %}
{% endif %} %}
{% from 'bootstrap/form.html' import render_form %}
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%}
{% extends 'themes/' + theme() + '/base.html' -%}
{% else -%}
{% extends "base.html" -%}
{% endif -%} -%}
{% from 'bootstrap/form.html' import render_form -%}
{% block app_content %}
{% block app_content -%}
<div class="row">
<div class="col col-login mx-auto">
<div class="card mt-5">
<div class="card-body p-6">
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div>
<ul class="option_list">
{% if current_user.is_authenticated %}
{% if post.user_id == current_user.id %}
{% if current_user.is_authenticated -%}
{% if post.user_id == current_user.id -%}
<li><a href="{{ url_for('post.post_edit', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
{{ _('Edit') }}</a></li>
{% endif %}
{% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() %}
<li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Delete') }}</a></li>
{% endif %}
{% if post.user_id == current_user.id and not post.mea_culpa %}
{% endif -%}
{% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
{% if post.deleted -%}
<li><a href="{{ url_for('post.post_restore', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Restore') }}</a></li>
{% else -%}
<li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Delete') }}</a></li>
{% endif -%}
{% endif -%}
{% if post.user_id == current_user.id and not post.mea_culpa -%}
<li><a href="{{ url_for('post.post_mea_culpa', post_id=post.id) }}" class="no-underline"><span class="fe fe-mea-culpa"></span>
{{ _("I made a mistake with this post and have changed my mind about the topic") }}</a></li>
{% endif %}
{% if post.user_id != current_user.id %}
{% endif -%}
{% if post.user_id != current_user.id -%}
<li><a href="{{ url_for('post.post_block_user', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _('Block post author @%(author_name)s', author_name=post.author.user_name) }}</a></li>
{% if post.community.is_moderator() or current_user.is_admin() %}
{% if post.community.is_moderator() or current_user.is_admin() -%}
<li><a href="{{ url_for('community.community_ban_user', community_id=post.community.id, user_id=post.author.id) }}" class="no-underline"><span class="fe fe-block red"></span>
{{ _('Ban post author @%(author_name)s from<br>%(community_name)s', author_name=post.author.user_name, community_name=post.community.title) }}</a></li>
{% endif %}
{% if post.domain_id %}
{% endif -%}
{% if post.domain_id -%}
<li><a href="{{ url_for('post.post_block_domain', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _('Block domain %(domain)s', domain=post.domain.name) }}</a></li>
{% endif %}
{% if post.instance_id and post.instance_id != 1 %}
{% endif -%}
{% if post.instance_id and post.instance_id != 1 -%}
<li><a href="{{ url_for('post.post_block_instance', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _("Hide every post from author's instance: %(name)s", name=post.instance.domain) }}</a></li>
{% endif %}
{% endif %}
{% endif %}
{% if post.ap_id %}
{% endif -%}
{% endif -%}
{% endif -%}
{% if post.ap_id -%}
<li><a href="{{ post.ap_id }}" rel="nofollow" class="no-underline"><span class="fe fe-external"></span>
{{ _('View original on %(domain)s', domain=post.instance.domain) }}</a></li>
{% endif %}
{% endif -%}
<li><a href="{{ url_for('post.post_report', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-report"></span>
{{ _('Report to moderators') }}</a></li>
</ul>
@ -54,4 +59,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock -%}

View file

@ -1,35 +1,40 @@
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
{% extends 'themes/' + theme() + '/base.html' %}
{% else %}
{% extends "base.html" %}
{% endif %} %}
{% from 'bootstrap/form.html' import render_form %}
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%}
{% extends 'themes/' + theme() + '/base.html' -%}
{% else -%}
{% extends "base.html" -%}
{% endif -%} -%}
{% from 'bootstrap/form.html' import render_form -%}
{% block app_content %}
{% block app_content -%}
<div class="row">
<div class="col col-login mx-auto">
<div class="card mt-5">
<div class="card-body p-6">
<div class="card-title">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div>
<ul class="option_list">
{% if current_user.is_authenticated %}
{% if post_reply.user_id == current_user.id %}
{% if current_user.is_authenticated -%}
{% if post_reply.user_id == current_user.id -%}
<li><a href="{{ url_for('post.post_reply_edit', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
{{ _('Edit') }}</a></li>
{% endif %}
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() %}
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Delete') }}</a></li>
{% endif %}
{% if post_reply.user_id != current_user.id %}
{% endif -%}
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
{% if post_reply.deleted -%}
<li><a href="{{ url_for('post.post_reply_restore', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Restore') }}</a></li>
{% else -%}
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Delete') }}</a></li>
{% endif -%}
{% endif -%}
{% if post_reply.user_id != current_user.id -%}
<li><a href="{{ url_for('post.post_reply_block_user', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _('Block author @%(author_name)s', author_name=post_reply.author.user_name) }}</a></li>
{% if post_reply.instance_id and post_reply.instance_id != 1 %}
{% if post_reply.instance_id and post_reply.instance_id != 1 -%}
<li><a href="{{ url_for('post.post_reply_block_instance', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _("Hide every post from author's instance: %(name)s", name=post_reply.instance.domain) }}</a></li>
{% endif %}
{% endif %}
{% endif %}
{% endif -%}
{% endif -%}
{% endif -%}
<li><a href="{{ url_for('post.post_reply_report', post_id=post.id, comment_id=post_reply.id) }}" rel="nofollow" class="no-underline"><span class="fe fe-report"></span>
{{ _('Report to moderators') }}</a></li>
</ul>
@ -38,4 +43,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock -%}

View file

@ -54,7 +54,7 @@ def show_topic(topic_path):
# filter out nsfw and nsfl if desired
if current_user.is_anonymous:
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False, Post.deleted == False)
content_filters = {}
else:
if current_user.ignore_bots:
@ -63,6 +63,7 @@ def show_topic(topic_path):
posts = posts.filter(Post.nsfl == False)
if current_user.show_nsfw is False:
posts = posts.filter(Post.nsfw == False)
posts = posts.filter(Post.deleted == False)
content_filters = user_filters_posts(current_user.id)
# filter blocked domains and instances
@ -138,7 +139,7 @@ def show_topic_rss(topic_path):
if topic:
posts = Post.query.join(Community, Post.community_id == Community.id).filter(Community.topic_id == topic.id,
Community.banned == False)
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False, Post.deleted == False)
posts = posts.order_by(desc(Post.created_at)).limit(100).all()
fg = FeedGenerator()

View file

@ -59,7 +59,7 @@ def show_profile(user):
post_page = request.args.get('post_page', 1, type=int)
replies_page = request.args.get('replies_page', 1, type=int)
posts = Post.query.filter_by(user_id=user.id).order_by(desc(Post.posted_at)).paginate(page=post_page, per_page=50, error_out=False)
posts = Post.query.filter_by(user_id=user.id).filter(Post.deleted == False).order_by(desc(Post.posted_at)).paginate(page=post_page, per_page=50, error_out=False)
moderates = Community.query.filter_by(banned=False).join(CommunityMember).filter(CommunityMember.user_id == user.id)\
.filter(or_(CommunityMember.is_moderator, CommunityMember.is_owner))
if current_user.is_authenticated and (user.id == current_user.get_id() or current_user.is_admin()):
@ -69,7 +69,7 @@ def show_profile(user):
subscribed = Community.query.filter_by(banned=False).join(CommunityMember).filter(CommunityMember.user_id == user.id).all()
if current_user.is_anonymous or user.id != current_user.id:
moderates = moderates.filter(Community.private_mods == False)
post_replies = PostReply.query.filter_by(user_id=user.id).order_by(desc(PostReply.posted_at)).paginate(page=replies_page, per_page=50, error_out=False)
post_replies = PostReply.query.filter_by(user_id=user.id).filter(PostReply.deleted == False).order_by(desc(PostReply.posted_at)).paginate(page=replies_page, per_page=50, error_out=False)
# profile info
canonical = user.ap_public_url if user.ap_public_url else None

View file

@ -0,0 +1,45 @@
"""soft deletes
Revision ID: 5e84668d279e
Revises: 2191fa36c09d
Create Date: 2024-06-02 14:50:17.295862
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5e84668d279e'
down_revision = '2191fa36c09d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('post', schema=None) as batch_op:
batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=True))
batch_op.create_index(batch_op.f('ix_post_deleted'), ['deleted'], unique=False)
with op.batch_alter_table('post_reply', schema=None) as batch_op:
batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=True))
batch_op.create_index(batch_op.f('ix_post_reply_deleted'), ['deleted'], unique=False)
op.execute(sa.DDL('UPDATE "post" SET deleted = false'))
op.execute(sa.DDL('UPDATE "post_reply" SET deleted = false'))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('post_reply', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_post_reply_deleted'))
batch_op.drop_column('deleted')
with op.batch_alter_table('post', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_post_deleted'))
batch_op.drop_column('deleted')
# ### end Alembic commands ###