mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
hashtag UI #184
This commit is contained in:
parent
f5c7e44d28
commit
3c5a86f6e6
26 changed files with 504 additions and 19 deletions
|
@ -103,6 +103,9 @@ def create_app(config_class=Config):
|
||||||
from app.search import bp as search_bp
|
from app.search import bp as search_bp
|
||||||
app.register_blueprint(search_bp)
|
app.register_blueprint(search_bp)
|
||||||
|
|
||||||
|
from app.tag import bp as tag_bp
|
||||||
|
app.register_blueprint(tag_bp)
|
||||||
|
|
||||||
# send error reports via email
|
# send error reports via email
|
||||||
if app.config['MAIL_SERVER'] and app.config['MAIL_ERRORS']:
|
if app.config['MAIL_SERVER'] and app.config['MAIL_ERRORS']:
|
||||||
auth = None
|
auth = None
|
||||||
|
|
|
@ -161,7 +161,8 @@ def post_to_activity(post: Post, community: Community):
|
||||||
'language': {
|
'language': {
|
||||||
'identifier': post.language_code(),
|
'identifier': post.language_code(),
|
||||||
'name': post.language_name()
|
'name': post.language_name()
|
||||||
}
|
},
|
||||||
|
'tag': post.tags_for_activitypub()
|
||||||
},
|
},
|
||||||
"cc": [
|
"cc": [
|
||||||
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}"
|
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}"
|
||||||
|
@ -208,7 +209,12 @@ def post_to_page(post: Post, community: Community):
|
||||||
"sensitive": post.nsfw or post.nsfl,
|
"sensitive": post.nsfw or post.nsfl,
|
||||||
"published": ap_datetime(post.created_at),
|
"published": ap_datetime(post.created_at),
|
||||||
"stickied": post.sticky,
|
"stickied": post.sticky,
|
||||||
"audience": f"https://{current_app.config['SERVER_NAME']}/c/{community.name}"
|
"audience": f"https://{current_app.config['SERVER_NAME']}/c/{community.name}",
|
||||||
|
"tag": post.tags_for_activitypub(),
|
||||||
|
'language': {
|
||||||
|
'identifier': post.language_code(),
|
||||||
|
'name': post.language_name()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if post.edited_at is not None:
|
if post.edited_at is not None:
|
||||||
activity_data["updated"] = ap_datetime(post.edited_at)
|
activity_data["updated"] = ap_datetime(post.edited_at)
|
||||||
|
@ -385,7 +391,7 @@ def find_hashtag_or_create(hashtag: str) -> Tag:
|
||||||
if existing_tag:
|
if existing_tag:
|
||||||
return existing_tag
|
return existing_tag
|
||||||
else:
|
else:
|
||||||
new_tag = Tag(name=hashtag.lower(), display_as=hashtag)
|
new_tag = Tag(name=hashtag.lower(), display_as=hashtag, post_count=1)
|
||||||
db.session.add(new_tag)
|
db.session.add(new_tag)
|
||||||
return new_tag
|
return new_tag
|
||||||
|
|
||||||
|
|
11
app/cli.py
11
app/cli.py
|
@ -7,7 +7,7 @@ from datetime import datetime, timedelta
|
||||||
import flask
|
import flask
|
||||||
from flask import json, current_app
|
from flask import json, current_app
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
from sqlalchemy import or_, desc
|
from sqlalchemy import or_, desc, text
|
||||||
from sqlalchemy.orm import configure_mappers
|
from sqlalchemy.orm import configure_mappers
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
@ -19,7 +19,8 @@ from app.auth.util import random_token
|
||||||
from app.constants import NOTIF_COMMUNITY, NOTIF_POST, NOTIF_REPLY
|
from app.constants import NOTIF_COMMUNITY, NOTIF_POST, NOTIF_REPLY
|
||||||
from app.email import send_verification_email, send_email
|
from app.email import send_verification_email, send_email
|
||||||
from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission, Domain, ActivityPubLog, \
|
from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission, Domain, ActivityPubLog, \
|
||||||
utcnow, Site, Instance, File, Notification, Post, CommunityMember, NotificationSubscription, PostReply, Language
|
utcnow, Site, Instance, File, Notification, Post, CommunityMember, NotificationSubscription, PostReply, Language, \
|
||||||
|
Tag
|
||||||
from app.utils import file_get_contents, retrieve_block_list, blocked_domains, retrieve_peertube_block_list, \
|
from app.utils import file_get_contents, retrieve_block_list, blocked_domains, retrieve_peertube_block_list, \
|
||||||
shorten_string
|
shorten_string
|
||||||
|
|
||||||
|
@ -168,6 +169,12 @@ def register(app):
|
||||||
db.session.query(ActivityPubLog).filter(ActivityPubLog.created_at < utcnow() - timedelta(days=3)).delete()
|
db.session.query(ActivityPubLog).filter(ActivityPubLog.created_at < utcnow() - timedelta(days=3)).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
for tag in Tag.query.all():
|
||||||
|
post_count = db.session.execute(text('SELECT COUNT(post_id) as c FROM "post_tag" WHERE tag_id = :tag_id'),
|
||||||
|
{ 'tag_id': tag.id}).scalar()
|
||||||
|
tag.post_count = post_count
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@app.cli.command("spaceusage")
|
@app.cli.command("spaceusage")
|
||||||
def spaceusage():
|
def spaceusage():
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
|
|
@ -93,9 +93,10 @@ class BanUserCommunityForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class CreateDiscussionForm(FlaskForm):
|
class CreateDiscussionForm(FlaskForm):
|
||||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
|
||||||
discussion_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
discussion_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||||
discussion_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
discussion_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||||
|
tags = StringField(_l('Tags'), validators=[Optional(), Length(min=3, max=5000)])
|
||||||
sticky = BooleanField(_l('Sticky'))
|
sticky = BooleanField(_l('Sticky'))
|
||||||
nsfw = BooleanField(_l('NSFW'))
|
nsfw = BooleanField(_l('NSFW'))
|
||||||
nsfl = BooleanField(_l('Gore/gross'))
|
nsfl = BooleanField(_l('Gore/gross'))
|
||||||
|
@ -105,11 +106,12 @@ class CreateDiscussionForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class CreateLinkForm(FlaskForm):
|
class CreateLinkForm(FlaskForm):
|
||||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
|
||||||
link_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
link_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||||
link_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
link_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||||
link_url = StringField(_l('URL'), validators=[DataRequired(), Regexp(r'^https?://', message='Submitted links need to start with "http://"" or "https://"')],
|
link_url = StringField(_l('URL'), validators=[DataRequired(), Regexp(r'^https?://', message='Submitted links need to start with "http://"" or "https://"')],
|
||||||
render_kw={'placeholder': 'https://...'})
|
render_kw={'placeholder': 'https://...'})
|
||||||
|
tags = StringField(_l('Tags'), validators=[Optional(), Length(min=3, max=5000)])
|
||||||
sticky = BooleanField(_l('Sticky'))
|
sticky = BooleanField(_l('Sticky'))
|
||||||
nsfw = BooleanField(_l('NSFW'))
|
nsfw = BooleanField(_l('NSFW'))
|
||||||
nsfl = BooleanField(_l('Gore/gross'))
|
nsfl = BooleanField(_l('Gore/gross'))
|
||||||
|
@ -126,11 +128,12 @@ class CreateLinkForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class CreateVideoForm(FlaskForm):
|
class CreateVideoForm(FlaskForm):
|
||||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
|
||||||
video_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
video_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||||
video_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
video_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||||
video_url = StringField(_l('URL'), validators=[DataRequired(), Regexp(r'^https?://', message='Submitted links need to start with "http://"" or "https://"')],
|
video_url = StringField(_l('URL'), validators=[DataRequired(), Regexp(r'^https?://', message='Submitted links need to start with "http://"" or "https://"')],
|
||||||
render_kw={'placeholder': 'https://...'})
|
render_kw={'placeholder': 'https://...'})
|
||||||
|
tags = StringField(_l('Tags'), validators=[Optional(), Length(min=3, max=5000)])
|
||||||
sticky = BooleanField(_l('Sticky'))
|
sticky = BooleanField(_l('Sticky'))
|
||||||
nsfw = BooleanField(_l('NSFW'))
|
nsfw = BooleanField(_l('NSFW'))
|
||||||
nsfl = BooleanField(_l('Gore/gross'))
|
nsfl = BooleanField(_l('Gore/gross'))
|
||||||
|
@ -147,11 +150,12 @@ class CreateVideoForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class CreateImageForm(FlaskForm):
|
class CreateImageForm(FlaskForm):
|
||||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
|
||||||
image_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
image_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||||
image_alt_text = StringField(_l('Alt text'), validators=[Optional(), Length(min=3, max=255)])
|
image_alt_text = StringField(_l('Alt text'), validators=[Optional(), Length(min=3, max=255)])
|
||||||
image_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
image_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||||
image_file = FileField(_l('Image'), validators=[DataRequired()])
|
image_file = FileField(_l('Image'), validators=[DataRequired()])
|
||||||
|
tags = StringField(_l('Tags'), validators=[Optional(), Length(min=3, max=5000)])
|
||||||
sticky = BooleanField(_l('Sticky'))
|
sticky = BooleanField(_l('Sticky'))
|
||||||
nsfw = BooleanField(_l('NSFW'))
|
nsfw = BooleanField(_l('NSFW'))
|
||||||
nsfl = BooleanField(_l('Gore/gross'))
|
nsfl = BooleanField(_l('Gore/gross'))
|
||||||
|
|
|
@ -796,7 +796,8 @@ def federate_post(community, post):
|
||||||
'language': {
|
'language': {
|
||||||
'identifier': post.language_code(),
|
'identifier': post.language_code(),
|
||||||
'name': post.language_name()
|
'name': post.language_name()
|
||||||
}
|
},
|
||||||
|
'tag': post.tags_for_activitypub()
|
||||||
}
|
}
|
||||||
create = {
|
create = {
|
||||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
||||||
|
@ -890,7 +891,8 @@ def federate_post_to_user_followers(post):
|
||||||
'language': {
|
'language': {
|
||||||
'identifier': post.language_code(),
|
'identifier': post.language_code(),
|
||||||
'name': post.language_name()
|
'name': post.language_name()
|
||||||
}
|
},
|
||||||
|
'tag': post.tags_for_activitypub()
|
||||||
}
|
}
|
||||||
create = {
|
create = {
|
||||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
||||||
|
|
|
@ -10,14 +10,15 @@ from pillow_heif import register_heif_opener
|
||||||
|
|
||||||
from app import db, cache, celery
|
from app import db, cache, celery
|
||||||
from app.activitypub.signature import post_request, default_context
|
from app.activitypub.signature import post_request, default_context
|
||||||
from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model, ensure_domains_match
|
from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model, ensure_domains_match, \
|
||||||
|
find_hashtag_or_create
|
||||||
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO, NOTIF_POST
|
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO, NOTIF_POST
|
||||||
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
|
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
|
||||||
Instance, Notification, User, ActivityPubLog, NotificationSubscription, Language
|
Instance, Notification, User, ActivityPubLog, NotificationSubscription, Language, Tag
|
||||||
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
|
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
|
||||||
is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
|
is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
|
||||||
remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases
|
remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases
|
||||||
from sqlalchemy import func, desc
|
from sqlalchemy import func, desc, text
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
@ -384,7 +385,10 @@ def save_post(form, post: Post, type: str):
|
||||||
return
|
return
|
||||||
|
|
||||||
db.session.add(post)
|
db.session.add(post)
|
||||||
db.session.commit()
|
else:
|
||||||
|
db.session.execute(text('DELETE FROM "post_tag" WHERE post_id = :post_id'), { 'post_id': post.id})
|
||||||
|
post.tags = tags_from_string(form.tags.data)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
# Notify author about replies
|
# Notify author about replies
|
||||||
# Remove any subscription that currently exists
|
# Remove any subscription that currently exists
|
||||||
|
@ -404,6 +408,22 @@ def save_post(form, post: Post, type: str):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def tags_from_string(tags: str) -> List[Tag]:
|
||||||
|
return_value = []
|
||||||
|
tags = tags.strip()
|
||||||
|
if tags == '':
|
||||||
|
return []
|
||||||
|
tag_list = tags.split(',')
|
||||||
|
tag_list = [tag.strip() for tag in tag_list]
|
||||||
|
for tag in tag_list:
|
||||||
|
if tag[0] == '#':
|
||||||
|
tag = tag[1:]
|
||||||
|
tag_to_append = find_hashtag_or_create(tag)
|
||||||
|
if tag_to_append:
|
||||||
|
return_value.append(tag_to_append)
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
|
||||||
def delete_post_from_community(post_id):
|
def delete_post_from_community(post_id):
|
||||||
if current_app.debug:
|
if current_app.debug:
|
||||||
delete_post_from_community_task(post_id)
|
delete_post_from_community_task(post_id)
|
||||||
|
|
|
@ -174,6 +174,8 @@ class Tag(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(256), index=True) # lowercase version of tag, e.g. solarstorm
|
name = db.Column(db.String(256), index=True) # lowercase version of tag, e.g. solarstorm
|
||||||
display_as = db.Column(db.String(256)) # Version of tag with uppercase letters, e.g. SolarStorm
|
display_as = db.Column(db.String(256)) # Version of tag with uppercase letters, e.g. SolarStorm
|
||||||
|
post_count = db.Column(db.Integer, default=0)
|
||||||
|
banned = db.Column(db.Boolean, default=False, index=True)
|
||||||
|
|
||||||
|
|
||||||
class Language(db.Model):
|
class Language(db.Model):
|
||||||
|
@ -1019,6 +1021,14 @@ class Post(db.Model):
|
||||||
else:
|
else:
|
||||||
return 'English'
|
return 'English'
|
||||||
|
|
||||||
|
def tags_for_activitypub(self):
|
||||||
|
return_value = []
|
||||||
|
for tag in self.tags:
|
||||||
|
return_value.append({'type': 'Hashtag',
|
||||||
|
'href': f'https://{current_app.config["SERVER_NAME"]}/tag/{tag.name}',
|
||||||
|
'name': f'#{tag.name}'})
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
|
||||||
class PostReply(db.Model):
|
class PostReply(db.Model):
|
||||||
query_class = FullTextSearchQuery
|
query_class = FullTextSearchQuery
|
||||||
|
|
|
@ -14,7 +14,7 @@ from app.community.util import save_post, send_to_remote_instance
|
||||||
from app.inoculation import inoculation
|
from app.inoculation import inoculation
|
||||||
from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm
|
from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm
|
||||||
from app.community.forms import CreateLinkForm, CreateImageForm, CreateDiscussionForm, CreateVideoForm
|
from app.community.forms import CreateLinkForm, CreateImageForm, CreateDiscussionForm, CreateVideoForm
|
||||||
from app.post.util import post_replies, get_comment_branch, post_reply_count
|
from app.post.util import post_replies, get_comment_branch, post_reply_count, tags_to_string
|
||||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_LINK, \
|
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_LINK, \
|
||||||
POST_TYPE_IMAGE, \
|
POST_TYPE_IMAGE, \
|
||||||
POST_TYPE_ARTICLE, POST_TYPE_VIDEO, NOTIF_REPLY, NOTIF_POST
|
POST_TYPE_ARTICLE, POST_TYPE_VIDEO, NOTIF_REPLY, NOTIF_POST
|
||||||
|
@ -863,6 +863,7 @@ def post_edit_discussion_post(post_id: int):
|
||||||
form.nsfl.data = post.nsfl
|
form.nsfl.data = post.nsfl
|
||||||
form.sticky.data = post.sticky
|
form.sticky.data = post.sticky
|
||||||
form.language_id.data = post.language_id
|
form.language_id.data = post.language_id
|
||||||
|
form.tags.data = tags_to_string(post)
|
||||||
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||||
form.sticky.render_kw = {'disabled': True}
|
form.sticky.render_kw = {'disabled': True}
|
||||||
return render_template('post/post_edit_discussion.html', title=_('Edit post'), form=form, post=post,
|
return render_template('post/post_edit_discussion.html', title=_('Edit post'), form=form, post=post,
|
||||||
|
@ -948,6 +949,7 @@ def post_edit_image_post(post_id: int):
|
||||||
form.nsfl.data = post.nsfl
|
form.nsfl.data = post.nsfl
|
||||||
form.sticky.data = post.sticky
|
form.sticky.data = post.sticky
|
||||||
form.language_id.data = post.language_id
|
form.language_id.data = post.language_id
|
||||||
|
form.tags.data = tags_to_string(post)
|
||||||
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||||
form.sticky.render_kw = {'disabled': True}
|
form.sticky.render_kw = {'disabled': True}
|
||||||
return render_template('post/post_edit_image.html', title=_('Edit post'), form=form, post=post,
|
return render_template('post/post_edit_image.html', title=_('Edit post'), form=form, post=post,
|
||||||
|
@ -1033,6 +1035,7 @@ def post_edit_link_post(post_id: int):
|
||||||
form.nsfl.data = post.nsfl
|
form.nsfl.data = post.nsfl
|
||||||
form.sticky.data = post.sticky
|
form.sticky.data = post.sticky
|
||||||
form.language_id.data = post.language_id
|
form.language_id.data = post.language_id
|
||||||
|
form.tags.data = tags_to_string(post)
|
||||||
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||||
form.sticky.render_kw = {'disabled': True}
|
form.sticky.render_kw = {'disabled': True}
|
||||||
return render_template('post/post_edit_link.html', title=_('Edit post'), form=form, post=post,
|
return render_template('post/post_edit_link.html', title=_('Edit post'), form=form, post=post,
|
||||||
|
@ -1118,6 +1121,7 @@ def post_edit_video_post(post_id: int):
|
||||||
form.nsfl.data = post.nsfl
|
form.nsfl.data = post.nsfl
|
||||||
form.sticky.data = post.sticky
|
form.sticky.data = post.sticky
|
||||||
form.language_id.data = post.language_id
|
form.language_id.data = post.language_id
|
||||||
|
form.tags.data = tags_to_string(post)
|
||||||
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||||
form.sticky.render_kw = {'disabled': True}
|
form.sticky.render_kw = {'disabled': True}
|
||||||
return render_template('post/post_edit_video.html', title=_('Edit post'), form=form, post=post,
|
return render_template('post/post_edit_video.html', title=_('Edit post'), form=form, post=post,
|
||||||
|
@ -1157,7 +1161,8 @@ def federate_post_update(post):
|
||||||
'language': {
|
'language': {
|
||||||
'identifier': post.language_code(),
|
'identifier': post.language_code(),
|
||||||
'name': post.language_name()
|
'name': post.language_name()
|
||||||
}
|
},
|
||||||
|
'tag': post.tags_for_activitypub()
|
||||||
}
|
}
|
||||||
update_json = {
|
update_json = {
|
||||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}",
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}",
|
||||||
|
@ -1241,7 +1246,8 @@ def federate_post_edit_to_user_followers(post):
|
||||||
'language': {
|
'language': {
|
||||||
'identifier': post.language_code(),
|
'identifier': post.language_code(),
|
||||||
'name': post.language_name()
|
'name': post.language_name()
|
||||||
}
|
},
|
||||||
|
'tag': post.tags_for_activitypub()
|
||||||
}
|
}
|
||||||
update = {
|
update = {
|
||||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask_login import current_user
|
||||||
from sqlalchemy import desc, text, or_
|
from sqlalchemy import desc, text, or_
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import PostReply
|
from app.models import PostReply, Post
|
||||||
from app.utils import blocked_instances, blocked_users
|
from app.utils import blocked_instances, blocked_users
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,3 +73,8 @@ def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[Post
|
||||||
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'),
|
||||||
{'post_id': post_id}).scalar()
|
{'post_id': post_id}).scalar()
|
||||||
|
|
||||||
|
|
||||||
|
def tags_to_string(post: Post) -> str:
|
||||||
|
if post.tags.count() > 0:
|
||||||
|
return ', '.join([tag.name for tag in post.tags])
|
||||||
|
|
|
@ -774,6 +774,14 @@ div.navbar {
|
||||||
border: dotted 2px transparent;
|
border: dotted 2px transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post_tags {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.post_tags .post_tag {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
/* high contrast */
|
/* high contrast */
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
:root {
|
:root {
|
||||||
|
|
|
@ -366,6 +366,15 @@ div.navbar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post_tags {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
.post_tag {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* high contrast */
|
/* high contrast */
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
|
|
5
app/tag/__init__.py
Normal file
5
app/tag/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('tag', __name__)
|
||||||
|
|
||||||
|
from app.tag import routes
|
133
app/tag/routes.py
Normal file
133
app/tag/routes.py
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
||||||
|
from flask_login import login_user, logout_user, current_user, login_required
|
||||||
|
from flask_babel import _
|
||||||
|
|
||||||
|
from app import db, constants, cache
|
||||||
|
from app.inoculation import inoculation
|
||||||
|
from app.models import Post, Community, Tag, post_tag
|
||||||
|
from app.tag import bp
|
||||||
|
from app.utils import render_template, permission_required, joined_communities, moderating_communities, \
|
||||||
|
user_filters_posts, blocked_instances, blocked_users, blocked_domains
|
||||||
|
from sqlalchemy import desc, or_
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/tag/<tag>', methods=['GET'])
|
||||||
|
def show_tag(tag):
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
|
||||||
|
tag = Tag.query.filter(Tag.name == tag.lower()).first()
|
||||||
|
if 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)
|
||||||
|
|
||||||
|
if current_user.is_anonymous or current_user.ignore_bots:
|
||||||
|
posts = posts.filter(Post.from_bot == False)
|
||||||
|
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
domains_ids = blocked_domains(current_user.id)
|
||||||
|
if domains_ids:
|
||||||
|
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
||||||
|
instance_ids = blocked_instances(current_user.id)
|
||||||
|
if instance_ids:
|
||||||
|
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
||||||
|
# filter blocked users
|
||||||
|
blocked_accounts = blocked_users(current_user.id)
|
||||||
|
if blocked_accounts:
|
||||||
|
posts = posts.filter(Post.user_id.not_in(blocked_accounts))
|
||||||
|
content_filters = user_filters_posts(current_user.id)
|
||||||
|
else:
|
||||||
|
content_filters = {}
|
||||||
|
|
||||||
|
posts = posts.order_by(desc(Post.posted_at))
|
||||||
|
|
||||||
|
# pagination
|
||||||
|
posts = posts.paginate(page=page, per_page=100, error_out=False)
|
||||||
|
next_url = url_for('tag.show_tag', tag=tag, page=posts.next_num) if posts.has_next else None
|
||||||
|
prev_url = url_for('tag.show_tag', tag=tag, page=posts.prev_num) if posts.has_prev and page != 1 else None
|
||||||
|
|
||||||
|
return render_template('tag/tag.html', tag=tag, title=tag.name, posts=posts,
|
||||||
|
POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE, POST_TYPE_LINK=constants.POST_TYPE_LINK,
|
||||||
|
POST_TYPE_VIDEO=constants.POST_TYPE_VIDEO,
|
||||||
|
next_url=next_url, prev_url=prev_url,
|
||||||
|
content_filters=content_filters,
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/tags', methods=['GET'])
|
||||||
|
def tags():
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
search = request.args.get('search', '')
|
||||||
|
|
||||||
|
tags = Tag.query.filter_by(banned=False)
|
||||||
|
if search != '':
|
||||||
|
tags = tags.filter(Tag.name.ilike(f'%{search}%'))
|
||||||
|
tags = tags.order_by(Tag.name)
|
||||||
|
tags = tags.paginate(page=page, per_page=100, error_out=False)
|
||||||
|
|
||||||
|
ban_visibility_permission = False
|
||||||
|
|
||||||
|
if not current_user.is_anonymous:
|
||||||
|
if not current_user.created_recently() and current_user.reputation > 100 or current_user.is_admin():
|
||||||
|
ban_visibility_permission = True
|
||||||
|
|
||||||
|
next_url = url_for('tag.tags', page=tags.next_num) if tags.has_next else None
|
||||||
|
prev_url = url_for('tag.tags', page=tags.prev_num) if tags.has_prev and page != 1 else None
|
||||||
|
|
||||||
|
return render_template('tag/tags.html', title='All known tags', tags=tags,
|
||||||
|
next_url=next_url, prev_url=prev_url, search=search, ban_visibility_permission=ban_visibility_permission)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/tags/banned', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def tags_blocked_list():
|
||||||
|
if not current_user.trustworthy():
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
search = request.args.get('search', '')
|
||||||
|
|
||||||
|
tags = Tag.query.filter_by(banned=True)
|
||||||
|
if search != '':
|
||||||
|
tags = tags.filter(Tag.name.ilike(f'%{search}%'))
|
||||||
|
tags = tags.order_by(Tag.name)
|
||||||
|
tags = tags.paginate(page=page, per_page=100, error_out=False)
|
||||||
|
|
||||||
|
next_url = url_for('tag.tags', page=tags.next_num) if tags.has_next else None
|
||||||
|
prev_url = url_for('tag.tags', page=tags.prev_num) if tags.has_prev and page != 1 else None
|
||||||
|
|
||||||
|
return render_template('tag/tags_blocked.html', title='Tags blocked on this instance', tags=tags,
|
||||||
|
next_url=next_url, prev_url=prev_url, search=search)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/tag/<tag>/ban')
|
||||||
|
@login_required
|
||||||
|
@permission_required('manage users')
|
||||||
|
def tag_ban(tag):
|
||||||
|
tag = Tag.query.filter(Tag.name == tag.lower()).first()
|
||||||
|
if tag:
|
||||||
|
tag.banned = True
|
||||||
|
db.session.commit()
|
||||||
|
tag.purge_content()
|
||||||
|
flash(_('%(name)s banned for all users and all content deleted.', name=tag.name))
|
||||||
|
return redirect(url_for('tag.tags'))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/tag/<tag>/unban')
|
||||||
|
@login_required
|
||||||
|
@permission_required('manage users')
|
||||||
|
def tag_unban(tag):
|
||||||
|
tag = Tag.query.filter(Tag.name == tag.lower()).first()
|
||||||
|
if tag:
|
||||||
|
tag.banned = False
|
||||||
|
db.session.commit()
|
||||||
|
flash(_('%(name)s un-banned for all users.', name=tag.name))
|
||||||
|
return redirect(url_for('tag.show_tag', tag=tag.name))
|
|
@ -44,6 +44,8 @@
|
||||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="discussion_body">{{ _('Enable markdown editor') }}</a>
|
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="discussion_body">{{ _('Enable markdown editor') }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
|
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|
|
@ -46,6 +46,8 @@
|
||||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="image_body">{{ _('Enable markdown editor') }}</a>
|
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="image_body">{{ _('Enable markdown editor') }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
|
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|
|
@ -45,6 +45,8 @@
|
||||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="link_body">{{ _('Enable markdown editor') }}</a>
|
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="link_body">{{ _('Enable markdown editor') }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
|
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|
|
@ -46,6 +46,8 @@
|
||||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="link_body">{{ _('Enable markdown editor') }}</a>
|
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="link_body">{{ _('Enable markdown editor') }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
|
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|
|
@ -138,7 +138,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="post_utilities_bar">
|
<div class="post_utilities_bar">
|
||||||
|
{% if post.tags.count() > 0 %}
|
||||||
|
<nav role="navigation">
|
||||||
|
<h3 class="visually-hidden">{{ _('Hashtags') }}</h3>
|
||||||
|
<ul class="post_tags">
|
||||||
|
{% for tag in post.tags %}
|
||||||
|
<li class="post_tag small"><a href="{{ url_for('tag.show_tag', tag=tag.name) }}">#{{ tag.display_as }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
{% if post.cross_posts %}
|
{% if post.cross_posts %}
|
||||||
<div class="cross_post_button">
|
<div class="cross_post_button">
|
||||||
<a href="{{ url_for('post.post_cross_posts', post_id=post.id) }}" aria-label="{{ _('Show cross-posts') }}"
|
<a href="{{ url_for('post.post_cross_posts', post_id=post.id) }}" aria-label="{{ _('Show cross-posts') }}"
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
|
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
|
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
|
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{{ render_field(form.notify_author) }}
|
{{ render_field(form.notify_author) }}
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
|
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{{ render_field(form.notify_author) }}
|
{{ render_field(form.notify_author) }}
|
||||||
|
|
72
app/templates/tag/tag.html
Normal file
72
app/templates/tag/tag.html
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
{% 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 %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/tags">{{ _('Tags') }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ tag.name|shorten }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h1 class="mt-2">{{ tag.name }}</h1>
|
||||||
|
<div class="post_list">
|
||||||
|
{% for post in posts.items %}
|
||||||
|
{% include 'post/_post_teaser.html' %}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ _('No posts in this tag yet.') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="Pagination" class="mt-4" role="navigation">
|
||||||
|
{% if prev_url %}
|
||||||
|
<a href="{{ prev_url }}" class="btn btn-primary" rel='nofollow'>
|
||||||
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_url %}
|
||||||
|
<a href="{{ next_url }}" class="btn btn-primary" rel='nofollow'>
|
||||||
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('Tag management') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
{% if user_access('ban users', current_user.id) or user_access('manage users', current_user.id) %}
|
||||||
|
{% if tag.banned %}
|
||||||
|
<div class="col-8">
|
||||||
|
<a class="w-100 btn btn-primary confirm_first" href="/tag/{{ tag.name }}/unban">{{ _('Unban') }}</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-8">
|
||||||
|
<a class="w-100 btn btn-primary confirm_first" href="/tag/{{ tag.name }}/ban">{{ _('Ban instance-wide') }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% include "_inoculation_links.html" %}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
66
app/templates/tag/tags.html
Normal file
66
app/templates/tag/tags.html
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
{% 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 %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
{% if search == '' %}
|
||||||
|
<h1>{{ _('Tags') }}</h1>
|
||||||
|
{% else %}
|
||||||
|
<h1>{{ _('Tags containing "%(search)s"', search=search) }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% if not current_user.is_anonymous and current_user.trustworthy() %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="/tags" class="btn {{ 'btn-primary' if request.path == '/tags' else 'btn-outline-secondary' }}">
|
||||||
|
{{ _('Tags') }}
|
||||||
|
</a>
|
||||||
|
<a href="/tags/banned" class="btn {{ 'btn-primary' if request.path == '/tags/banned' else 'btn-outline-secondary' }}">
|
||||||
|
{{ _('Banned tags') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<form method="get"><input type="search" name="search" value="{{ search }}" placeholder="{{ _('Search') }}" autofocus></form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="table-responsive-sm pt-4">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>Tag</th>
|
||||||
|
<th><span title="{{ _('How many times has something using this tag been posted') }}"># Posts</span></th>
|
||||||
|
</tr>
|
||||||
|
{% for tag in tags %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ url_for('tag.show_tag', tag=tag.name) }}">{{ tag.display_as }}</a></td>
|
||||||
|
<td>{{ tag.post_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<nav aria-label="Pagination" class="mt-4" role="navigation">
|
||||||
|
{% if prev_url %}
|
||||||
|
<a href="{{ prev_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_url %}
|
||||||
|
<a href="{{ next_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
66
app/templates/tag/tags_blocked.html
Normal file
66
app/templates/tag/tags_blocked.html
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
{% 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 %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
{% if search == '' %}
|
||||||
|
<h1>{{ _('Tags') }}</h1>
|
||||||
|
{% else %}
|
||||||
|
<h1>{{ _('Tags containing "%(search)s"', search=search) }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% if not current_user.is_anonymous and current_user.trustworthy() %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="/tags" class="btn {{ 'btn-primary' if request.path == '/tags' else 'btn-outline-secondary' }}">
|
||||||
|
{{ _('Tags') }}
|
||||||
|
</a>
|
||||||
|
<a href="/tags/banned" class="btn {{ 'btn-primary' if request.path == '/tags/banned' else 'btn-outline-secondary' }}">
|
||||||
|
{{ _('Banned tags') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<form method="get"><input type="search" name="search" value="{{ search }}" placeholder="{{ _('Search') }}" autofocus></form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="table-responsive-sm pt-4">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>Tag</th>
|
||||||
|
<th><span title="{{ _('How many times has something using this tag been posted') }}"># Posts</span></th>
|
||||||
|
</tr>
|
||||||
|
{% for tag in tags %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ url_for('tag.show_tag', tag=tag.name) }}">{{ tag.display_as }}</a></td>
|
||||||
|
<td>{{ tag.post_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<nav aria-label="Pagination" class="mt-4" role="navigation">
|
||||||
|
{% if prev_url %}
|
||||||
|
<a href="{{ prev_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_url %}
|
||||||
|
<a href="{{ next_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
36
migrations/versions/9752fb47d7a6_tag_ban.py
Normal file
36
migrations/versions/9752fb47d7a6_tag_ban.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""tag ban
|
||||||
|
|
||||||
|
Revision ID: 9752fb47d7a6
|
||||||
|
Revises: 20ee24728510
|
||||||
|
Create Date: 2024-05-12 12:46:09.954438
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9752fb47d7a6'
|
||||||
|
down_revision = '20ee24728510'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tag', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('post_count', sa.Integer(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('banned', sa.Boolean(), nullable=True))
|
||||||
|
batch_op.create_index(batch_op.f('ix_tag_banned'), ['banned'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tag', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_tag_banned'))
|
||||||
|
batch_op.drop_column('banned')
|
||||||
|
batch_op.drop_column('post_count')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Add table
Reference in a new issue