mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
soft deletes and deleted content admin area #193
This commit is contained in:
parent
f4931b474c
commit
2380a3ae61
19 changed files with 326 additions and 107 deletions
|
@ -1010,7 +1010,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
||||||
post.delete_dependencies()
|
post.delete_dependencies()
|
||||||
post.community.post_count -= 1
|
post.community.post_count -= 1
|
||||||
announce_activity_to_followers(post.community, post.author, request_json)
|
announce_activity_to_followers(post.community, post.author, request_json)
|
||||||
db.session.delete(post)
|
post.deleted = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
activity_log.result = 'success'
|
activity_log.result = 'success'
|
||||||
else:
|
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_html = '<p><em>deleted</em></p>'
|
||||||
reply.body = 'deleted'
|
reply.body = 'deleted'
|
||||||
reply.post.reply_count -= 1
|
reply.post.reply_count -= 1
|
||||||
|
reply.deleted = True
|
||||||
announce_activity_to_followers(reply.community, reply.author, request_json)
|
announce_activity_to_followers(reply.community, reply.author, request_json)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
activity_log.result = 'success'
|
activity_log.result = 'success'
|
||||||
|
@ -1213,7 +1214,7 @@ def community_outbox(actor):
|
||||||
actor = actor.strip()
|
actor = actor.strip()
|
||||||
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||||
if community is not None:
|
if community is not None:
|
||||||
posts = community.posts.limit(50).all()
|
posts = community.posts.filter(Post.deleted == False).limit(50).all()
|
||||||
|
|
||||||
community_data = {
|
community_data = {
|
||||||
"@context": default_context(),
|
"@context": default_context(),
|
||||||
|
@ -1234,7 +1235,7 @@ def community_featured(actor):
|
||||||
actor = actor.strip()
|
actor = actor.strip()
|
||||||
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||||
if community is not None:
|
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 = {
|
community_data = {
|
||||||
"@context": default_context(),
|
"@context": default_context(),
|
||||||
|
|
|
@ -78,11 +78,11 @@ def active_day():
|
||||||
|
|
||||||
|
|
||||||
def local_posts():
|
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():
|
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():
|
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 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):
|
if isinstance(to_delete, Post):
|
||||||
to_delete.delete_dependencies()
|
to_delete.delete_dependencies()
|
||||||
db.session.delete(to_delete)
|
to_delete.deleted = True
|
||||||
community.post_count -= 1
|
community.post_count -= 1
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
elif isinstance(to_delete, PostReply):
|
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)
|
to_delete.body_html = lemmy_markdown_to_html(to_delete.body)
|
||||||
else:
|
else:
|
||||||
to_delete.delete_dependencies()
|
to_delete.delete_dependencies()
|
||||||
db.session.delete(to_delete)
|
to_delete.deleted = True
|
||||||
|
|
||||||
db.session.commit()
|
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
|
# community bans by mods
|
||||||
elif community and community.is_moderator(deletor):
|
elif community and community.is_moderator(deletor):
|
||||||
post_replies = PostReply.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)
|
posts = Post.query.filter_by(user_id=user.id, community_id=community.id, deleted=False)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
for pr in post_replies:
|
for post_reply in post_replies:
|
||||||
pr.post.reply_count -= 1
|
post_reply.post.reply_count -= 1
|
||||||
if pr.has_replies():
|
if post_reply.has_replies():
|
||||||
pr.body = 'Banned'
|
post_reply.body = 'Banned'
|
||||||
pr.body_html = lemmy_markdown_to_html(pr.body)
|
post_reply.body_html = lemmy_markdown_to_html(post_reply.body)
|
||||||
else:
|
else:
|
||||||
pr.delete_dependencies()
|
post_reply.delete_dependencies()
|
||||||
db.session.delete(pr)
|
post_reply.deleted = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
for p in posts:
|
for post in posts:
|
||||||
if p.cross_posts:
|
if post.cross_posts:
|
||||||
old_cross_posts = Post.query.filter(Post.id.in_(p.cross_posts)).all()
|
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||||
for ocp in old_cross_posts:
|
for ocp in old_cross_posts:
|
||||||
if ocp.cross_posts is not None:
|
if ocp.cross_posts is not None:
|
||||||
ocp.cross_posts.remove(p.id)
|
ocp.cross_posts.remove(post.id)
|
||||||
p.delete_dependencies()
|
post.delete_dependencies()
|
||||||
db.session.delete(p)
|
post.deleted = True
|
||||||
p.community.post_count -= 1
|
post.community.post_count -= 1
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -552,7 +552,7 @@ def admin_content_trash():
|
||||||
|
|
||||||
page = request.args.get('page', 1, type=int)
|
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)
|
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
|
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).\
|
posts = Post.query.join(User, User.id == Post.user_id).\
|
||||||
filter(User.created > utcnow() - timedelta(days=3)).\
|
filter(User.created > utcnow() - timedelta(days=3)).\
|
||||||
filter(Post.posted_at > utcnow() - timedelta(days=3)).\
|
filter(Post.posted_at > utcnow() - timedelta(days=3)).\
|
||||||
|
filter(Post.deleted == False).\
|
||||||
filter(Post.score <= 0).order_by(Post.score)
|
filter(Post.score <= 0).order_by(Post.score)
|
||||||
posts = posts.paginate(page=page, per_page=100, error_out=False)
|
posts = posts.paginate(page=page, per_page=100, error_out=False)
|
||||||
|
|
||||||
post_replies = PostReply.query.join(User, User.id == PostReply.user_id). \
|
post_replies = PostReply.query.join(User, User.id == PostReply.user_id). \
|
||||||
filter(User.created > utcnow() - timedelta(days=3)). \
|
filter(User.created > utcnow() - timedelta(days=3)). \
|
||||||
filter(PostReply.posted_at > utcnow() - timedelta(days=3)). \
|
filter(PostReply.posted_at > utcnow() - timedelta(days=3)). \
|
||||||
|
filter(PostReply.deleted == False). \
|
||||||
filter(PostReply.score <= 0).order_by(PostReply.score)
|
filter(PostReply.score <= 0).order_by(PostReply.score)
|
||||||
post_replies = post_replies.paginate(page=replies_page, per_page=100, error_out=False)
|
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'])
|
@bp.route('/approve_registrations', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('approve registrations')
|
@permission_required('approve registrations')
|
||||||
|
|
|
@ -266,6 +266,15 @@ def register(app):
|
||||||
InstanceRole.role == 'admin').delete()
|
InstanceRole.role == 'admin').delete()
|
||||||
db.session.commit()
|
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")
|
@app.cli.command("spaceusage")
|
||||||
def spaceusage():
|
def spaceusage():
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
|
|
@ -197,7 +197,7 @@ def show_community(community: Community):
|
||||||
|
|
||||||
# filter out nsfw and nsfl if desired
|
# filter out nsfw and nsfl if desired
|
||||||
if current_user.is_anonymous:
|
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 = {}
|
content_filters = {}
|
||||||
else:
|
else:
|
||||||
if current_user.ignore_bots:
|
if current_user.ignore_bots:
|
||||||
|
@ -207,6 +207,7 @@ def show_community(community: Community):
|
||||||
if current_user.show_nsfw is False:
|
if current_user.show_nsfw is False:
|
||||||
posts = posts.filter(Post.nsfw == False)
|
posts = posts.filter(Post.nsfw == False)
|
||||||
content_filters = user_filters_posts(current_user.id)
|
content_filters = user_filters_posts(current_user.id)
|
||||||
|
posts = posts.filter(Post.deleted == False)
|
||||||
|
|
||||||
# filter domains and instances
|
# filter domains and instances
|
||||||
domains_ids = blocked_domains(current_user.id)
|
domains_ids = blocked_domains(current_user.id)
|
||||||
|
@ -319,7 +320,7 @@ def show_community_rss(actor):
|
||||||
if request_etag_matches(current_etag):
|
if request_etag_matches(current_etag):
|
||||||
return return_304(current_etag, 'application/rss+xml')
|
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
|
description = shorten_string(community.description, 150) if community.description else None
|
||||||
og_image = community.image.source_url if community.image_id else None
|
og_image = community.image.source_url if community.image_id else None
|
||||||
fg = FeedGenerator()
|
fg = FeedGenerator()
|
||||||
|
|
|
@ -541,7 +541,7 @@ def delete_post_from_community_task(post_id):
|
||||||
post = Post.query.get(post_id)
|
post = Post.query.get(post_id)
|
||||||
community = post.community
|
community = post.community
|
||||||
post.delete_dependencies()
|
post.delete_dependencies()
|
||||||
db.session.delete(post)
|
post.deleted = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if not community.local_only:
|
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)
|
post_reply.body_html = markdown_to_html(post_reply.body)
|
||||||
else:
|
else:
|
||||||
post_reply.delete_dependencies()
|
post_reply.delete_dependencies()
|
||||||
db.session.delete(post_reply)
|
post_reply.deleted = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# federate delete
|
# federate delete
|
||||||
|
|
|
@ -26,10 +26,10 @@ def show_domain(domain_id):
|
||||||
if domain:
|
if domain:
|
||||||
if current_user.is_anonymous or current_user.ignore_bots:
|
if current_user.is_anonymous or current_user.ignore_bots:
|
||||||
posts = Post.query.join(Community, Community.id == Post.community_id).\
|
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))
|
order_by(desc(Post.posted_at))
|
||||||
else:
|
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:
|
if current_user.is_authenticated:
|
||||||
instance_ids = blocked_instances(current_user.id)
|
instance_ids = blocked_instances(current_user.id)
|
||||||
|
|
|
@ -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, \
|
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
|
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, \
|
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
|
from PIL import Image
|
||||||
import pytesseract
|
import pytesseract
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ def home_page(type, sort):
|
||||||
|
|
||||||
if current_user.is_anonymous:
|
if current_user.is_anonymous:
|
||||||
flash(_('Create an account to tailor this feed to your interests.'))
|
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)
|
posts = posts.join(Community, Community.id == Post.community_id)
|
||||||
if type == 'home':
|
if type == 'home':
|
||||||
posts = posts.filter(Community.show_home == True)
|
posts = posts.filter(Community.show_home == True)
|
||||||
|
@ -368,7 +368,7 @@ def robots():
|
||||||
@bp.route('/sitemap.xml')
|
@bp.route('/sitemap.xml')
|
||||||
@cache.cached(timeout=6000)
|
@cache.cached(timeout=6000)
|
||||||
def sitemap():
|
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.join(Community, Community.id == Post.community_id)
|
||||||
posts = posts.filter(Community.show_all == True, Community.ap_id == None) # sitemap.xml only includes local posts
|
posts = posts.filter(Community.show_all == True, Community.ap_id == None) # sitemap.xml only includes local posts
|
||||||
if not g.site.enable_nsfw:
|
if not g.site.enable_nsfw:
|
||||||
|
@ -396,6 +396,10 @@ def list_files(directory):
|
||||||
@bp.route('/test')
|
@bp.route('/test')
|
||||||
def 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'
|
return 'done'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -880,7 +880,7 @@ class User(UserMixin, db.Model):
|
||||||
db.session.query(Notification).filter(Notification.user_id == self.id).delete()
|
db.session.query(Notification).filter(Notification.user_id == self.id).delete()
|
||||||
db.session.query(PollChoiceVote).filter(PollChoiceVote.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()
|
files = File.query.join(Post).filter(Post.user_id == self.id).all()
|
||||||
for file in files:
|
for file in files:
|
||||||
file.delete_from_disk()
|
file.delete_from_disk()
|
||||||
|
@ -888,12 +888,18 @@ class User(UserMixin, db.Model):
|
||||||
posts = Post.query.filter_by(user_id=self.id).all()
|
posts = Post.query.filter_by(user_id=self.id).all()
|
||||||
for post in posts:
|
for post in posts:
|
||||||
post.delete_dependencies()
|
post.delete_dependencies()
|
||||||
db.session.delete(post)
|
if soft:
|
||||||
|
post.deleted = True
|
||||||
|
else:
|
||||||
|
db.session.delete(post)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
post_replies = PostReply.query.filter_by(user_id=self.id).all()
|
post_replies = PostReply.query.filter_by(user_id=self.id).all()
|
||||||
for reply in post_replies:
|
for reply in post_replies:
|
||||||
reply.delete_dependencies()
|
reply.delete_dependencies()
|
||||||
db.session.delete(reply)
|
if soft:
|
||||||
|
reply.deleted = True
|
||||||
|
else:
|
||||||
|
db.session.delete(reply)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def mention_tag(self):
|
def mention_tag(self):
|
||||||
|
@ -938,6 +944,7 @@ class Post(db.Model):
|
||||||
body_html = db.Column(db.Text)
|
body_html = db.Column(db.Text)
|
||||||
type = db.Column(db.Integer)
|
type = db.Column(db.Integer)
|
||||||
comments_enabled = db.Column(db.Boolean, default=True)
|
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)
|
mea_culpa = db.Column(db.Boolean, default=False)
|
||||||
has_embed = db.Column(db.Boolean, default=False)
|
has_embed = db.Column(db.Boolean, default=False)
|
||||||
reply_count = db.Column(db.Integer, default=0)
|
reply_count = db.Column(db.Integer, default=0)
|
||||||
|
@ -1066,6 +1073,7 @@ class PostReply(db.Model):
|
||||||
notify_author = db.Column(db.Boolean, default=True)
|
notify_author = db.Column(db.Boolean, default=True)
|
||||||
created_at = db.Column(db.DateTime, index=True, default=utcnow)
|
created_at = db.Column(db.DateTime, index=True, default=utcnow)
|
||||||
posted_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))
|
ip = db.Column(db.String(50))
|
||||||
from_bot = db.Column(db.Boolean, default=False)
|
from_bot = db.Column(db.Boolean, default=False)
|
||||||
up_votes = db.Column(db.Integer, default=0)
|
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()
|
return PostReply.query.filter_by(parent_id=self.id).all()
|
||||||
|
|
||||||
def has_replies(self):
|
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
|
return reply is not None
|
||||||
|
|
||||||
def blocked_by_content_filter(self, content_filters):
|
def blocked_by_content_filter(self, content_filters):
|
||||||
|
|
|
@ -36,7 +36,7 @@ def show_post(post_id: int):
|
||||||
post = Post.query.get_or_404(post_id)
|
post = Post.query.get_or_404(post_id)
|
||||||
community: Community = post.community
|
community: Community = post.community
|
||||||
|
|
||||||
if community.banned:
|
if community.banned or post.deleted:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
sort = request.args.get('sort', 'hot')
|
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
|
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
|
description = shorten_string(markdown_to_text(post.body), 150) if post.body else None
|
||||||
|
|
||||||
|
# Breadcrumbs
|
||||||
breadcrumbs = []
|
breadcrumbs = []
|
||||||
breadcrumb = namedtuple("Breadcrumb", ['text', 'url'])
|
breadcrumb = namedtuple("Breadcrumb", ['text', 'url'])
|
||||||
breadcrumb.text = _('Home')
|
breadcrumb.text = _('Home')
|
||||||
|
@ -288,15 +289,15 @@ def show_post(post_id: int):
|
||||||
poll_total_votes = 0
|
poll_total_votes = 0
|
||||||
if post.type == POST_TYPE_POLL:
|
if post.type == POST_TYPE_POLL:
|
||||||
poll_data = Poll.query.get(post.id)
|
poll_data = Poll.query.get(post.id)
|
||||||
poll_choices = PollChoice.query.filter_by(post_id=post.id).order_by(PollChoice.sort_order).all()
|
if poll_data:
|
||||||
poll_total_votes = poll_data.total_votes()
|
poll_choices = PollChoice.query.filter_by(post_id=post.id).order_by(PollChoice.sort_order).all()
|
||||||
# Show poll results to everyone after the poll finishes, to the poll creator and to those who have voted
|
poll_total_votes = poll_data.total_votes()
|
||||||
if (current_user.is_authenticated and (poll_data.has_voted(current_user.id))) \
|
# Show poll results to everyone after the poll finishes, to the poll creator and to those who have voted
|
||||||
or poll_data.end_poll < datetime.utcnow():
|
if (current_user.is_authenticated and (poll_data.has_voted(current_user.id))) \
|
||||||
|
or poll_data.end_poll < datetime.utcnow():
|
||||||
poll_results = True
|
poll_results = True
|
||||||
else:
|
else:
|
||||||
poll_form = True
|
poll_form = True
|
||||||
|
|
||||||
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
|
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,
|
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):
|
def continue_discussion(post_id, comment_id):
|
||||||
post = Post.query.get_or_404(post_id)
|
post = Post.query.get_or_404(post_id)
|
||||||
comment = PostReply.query.get_or_404(comment_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)
|
abort(404)
|
||||||
mods = post.community.moderators()
|
mods = post.community.moderators()
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
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'])
|
@bp.route('/post/<int:post_id>/options', methods=['GET'])
|
||||||
def post_options(post_id: int):
|
def post_options(post_id: int):
|
||||||
post = Post.query.get_or_404(post_id)
|
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,
|
return render_template('post/post_options.html', post=post,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_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):
|
def post_reply_options(post_id: int, comment_id: int):
|
||||||
post = Post.query.get_or_404(post_id)
|
post = Post.query.get_or_404(post_id)
|
||||||
post_reply = PostReply.query.get_or_404(comment_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,
|
return render_template('post/post_reply_options.html', post=post, post_reply=post_reply,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_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:
|
if ocp.cross_posts is not None:
|
||||||
ocp.cross_posts.remove(post.id)
|
ocp.cross_posts.remove(post.id)
|
||||||
post.delete_dependencies()
|
post.delete_dependencies()
|
||||||
db.session.delete(post)
|
post.deleted = True
|
||||||
g.site.last_active = community.last_active = utcnow()
|
g.site.last_active = community.last_active = utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Post deleted.'))
|
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))
|
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'])
|
@bp.route('/post/<int:post_id>/report', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def post_report(post_id: int):
|
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)
|
post_reply.body_html = markdown_to_html(post_reply.body)
|
||||||
else:
|
else:
|
||||||
post_reply.delete_dependencies()
|
post_reply.delete_dependencies()
|
||||||
db.session.delete(post_reply)
|
post_reply.deleted = True
|
||||||
g.site.last_active = community.last_active = utcnow()
|
g.site.last_active = community.last_active = utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Comment deleted.'))
|
flash(_('Comment deleted.'))
|
||||||
|
|
|
@ -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
|
# 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]:
|
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:
|
if current_user.is_authenticated:
|
||||||
instance_ids = blocked_instances(current_user.id)
|
instance_ids = blocked_instances(current_user.id)
|
||||||
if instance_ids:
|
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:
|
if parent_comment is None:
|
||||||
return []
|
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:
|
if current_user.is_authenticated:
|
||||||
instance_ids = blocked_instances(current_user.id)
|
instance_ids = blocked_instances(current_user.id)
|
||||||
if instance_ids:
|
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
|
# The number of replies a post has
|
||||||
def post_reply_count(post_id) -> int:
|
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()
|
{'post_id': post_id}).scalar()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ def show_tag(tag):
|
||||||
|
|
||||||
posts = Post.query.join(Community, Community.id == Post.community_id). \
|
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). \
|
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:
|
if current_user.is_anonymous or current_user.ignore_bots:
|
||||||
posts = posts.filter(Post.from_bot == False)
|
posts = posts.filter(Post.from_bot == False)
|
||||||
|
|
41
app/templates/admin/deleted_posts.html
Normal file
41
app/templates/admin/deleted_posts.html
Normal 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 %}
|
|
@ -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_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_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_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' %}
|
{% 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>
|
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_approve_registrations' }}" href="{{ url_for('admin.admin_approve_registrations') }}">{{ _('Registration applications') }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,51 +1,56 @@
|
||||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%}
|
||||||
{% extends 'themes/' + theme() + '/base.html' %}
|
{% extends 'themes/' + theme() + '/base.html' -%}
|
||||||
{% else %}
|
{% else -%}
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" -%}
|
||||||
{% endif %} %}
|
{% endif -%} -%}
|
||||||
{% from 'bootstrap/form.html' import render_form %}
|
{% from 'bootstrap/form.html' import render_form -%}
|
||||||
|
|
||||||
{% block app_content %}
|
{% block app_content -%}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-login mx-auto">
|
<div class="col col-login mx-auto">
|
||||||
<div class="card mt-5">
|
<div class="card mt-5">
|
||||||
<div class="card-body p-6">
|
<div class="card-body p-6">
|
||||||
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div>
|
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div>
|
||||||
<ul class="option_list">
|
<ul class="option_list">
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated -%}
|
||||||
{% if post.user_id == current_user.id %}
|
{% 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>
|
<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>
|
{{ _('Edit') }}</a></li>
|
||||||
{% endif %}
|
{% 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.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>
|
{% if post.deleted -%}
|
||||||
{{ _('Delete') }}</a></li>
|
<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>
|
||||||
{% endif %}
|
{{ _('Restore') }}</a></li>
|
||||||
{% if post.user_id == current_user.id and not post.mea_culpa %}
|
{% 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>
|
<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>
|
{{ _("I made a mistake with this post and have changed my mind about the topic") }}</a></li>
|
||||||
{% endif %}
|
{% endif -%}
|
||||||
{% if post.user_id != current_user.id %}
|
{% 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>
|
<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>
|
{{ _('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>
|
<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>
|
{{ _('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 %}
|
{% endif -%}
|
||||||
{% if post.domain_id %}
|
{% 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>
|
<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>
|
{{ _('Block domain %(domain)s', domain=post.domain.name) }}</a></li>
|
||||||
{% endif %}
|
{% endif -%}
|
||||||
{% if post.instance_id and post.instance_id != 1 %}
|
{% 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>
|
<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>
|
{{ _("Hide every post from author's instance: %(name)s", name=post.instance.domain) }}</a></li>
|
||||||
{% endif %}
|
{% endif -%}
|
||||||
{% endif %}
|
{% endif -%}
|
||||||
{% endif %}
|
{% endif -%}
|
||||||
{% if post.ap_id %}
|
{% if post.ap_id -%}
|
||||||
<li><a href="{{ post.ap_id }}" rel="nofollow" class="no-underline"><span class="fe fe-external"></span>
|
<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>
|
{{ _('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>
|
<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>
|
{{ _('Report to moderators') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -54,4 +59,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock -%}
|
|
@ -1,35 +1,40 @@
|
||||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%}
|
||||||
{% extends 'themes/' + theme() + '/base.html' %}
|
{% extends 'themes/' + theme() + '/base.html' -%}
|
||||||
{% else %}
|
{% else -%}
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" -%}
|
||||||
{% endif %} %}
|
{% endif -%} -%}
|
||||||
{% from 'bootstrap/form.html' import render_form %}
|
{% from 'bootstrap/form.html' import render_form -%}
|
||||||
|
|
||||||
{% block app_content %}
|
{% block app_content -%}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-login mx-auto">
|
<div class="col col-login mx-auto">
|
||||||
<div class="card mt-5">
|
<div class="card mt-5">
|
||||||
<div class="card-body p-6">
|
<div class="card-body p-6">
|
||||||
<div class="card-title">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div>
|
<div class="card-title">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div>
|
||||||
<ul class="option_list">
|
<ul class="option_list">
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated -%}
|
||||||
{% if post_reply.user_id == current_user.id %}
|
{% 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>
|
<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>
|
{{ _('Edit') }}</a></li>
|
||||||
{% endif %}
|
{% 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.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>
|
{% if post_reply.deleted -%}
|
||||||
{{ _('Delete') }}</a></li>
|
<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>
|
||||||
{% endif %}
|
{{ _('Restore') }}</a></li>
|
||||||
{% if post_reply.user_id != current_user.id %}
|
{% 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>
|
<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>
|
{{ _('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>
|
<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>
|
{{ _("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>
|
<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>
|
{{ _('Report to moderators') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -38,4 +43,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock -%}
|
|
@ -54,7 +54,7 @@ def show_topic(topic_path):
|
||||||
|
|
||||||
# filter out nsfw and nsfl if desired
|
# filter out nsfw and nsfl if desired
|
||||||
if current_user.is_anonymous:
|
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 = {}
|
content_filters = {}
|
||||||
else:
|
else:
|
||||||
if current_user.ignore_bots:
|
if current_user.ignore_bots:
|
||||||
|
@ -63,6 +63,7 @@ def show_topic(topic_path):
|
||||||
posts = posts.filter(Post.nsfl == False)
|
posts = posts.filter(Post.nsfl == False)
|
||||||
if current_user.show_nsfw is False:
|
if current_user.show_nsfw is False:
|
||||||
posts = posts.filter(Post.nsfw == False)
|
posts = posts.filter(Post.nsfw == False)
|
||||||
|
posts = posts.filter(Post.deleted == False)
|
||||||
content_filters = user_filters_posts(current_user.id)
|
content_filters = user_filters_posts(current_user.id)
|
||||||
|
|
||||||
# filter blocked domains and instances
|
# filter blocked domains and instances
|
||||||
|
@ -138,7 +139,7 @@ def show_topic_rss(topic_path):
|
||||||
if topic:
|
if topic:
|
||||||
posts = Post.query.join(Community, Post.community_id == Community.id).filter(Community.topic_id == topic.id,
|
posts = Post.query.join(Community, Post.community_id == Community.id).filter(Community.topic_id == topic.id,
|
||||||
Community.banned == False)
|
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()
|
posts = posts.order_by(desc(Post.created_at)).limit(100).all()
|
||||||
|
|
||||||
fg = FeedGenerator()
|
fg = FeedGenerator()
|
||||||
|
|
|
@ -59,7 +59,7 @@ def show_profile(user):
|
||||||
post_page = request.args.get('post_page', 1, type=int)
|
post_page = request.args.get('post_page', 1, type=int)
|
||||||
replies_page = request.args.get('replies_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)\
|
moderates = Community.query.filter_by(banned=False).join(CommunityMember).filter(CommunityMember.user_id == user.id)\
|
||||||
.filter(or_(CommunityMember.is_moderator, CommunityMember.is_owner))
|
.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()):
|
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()
|
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:
|
if current_user.is_anonymous or user.id != current_user.id:
|
||||||
moderates = moderates.filter(Community.private_mods == False)
|
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
|
# profile info
|
||||||
canonical = user.ap_public_url if user.ap_public_url else None
|
canonical = user.ap_public_url if user.ap_public_url else None
|
||||||
|
|
45
migrations/versions/5e84668d279e_soft_deletes.py
Normal file
45
migrations/versions/5e84668d279e_soft_deletes.py
Normal 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 ###
|
Loading…
Reference in a new issue