mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
community wiki #127
This commit is contained in:
parent
55e4289a6e
commit
2c3f1763b3
10 changed files with 448 additions and 13 deletions
|
@ -63,6 +63,19 @@ class EditCommunityForm(FlaskForm):
|
||||||
submit = SubmitField(_l('Save'))
|
submit = SubmitField(_l('Save'))
|
||||||
|
|
||||||
|
|
||||||
|
class EditCommunityWikiPageForm(FlaskForm):
|
||||||
|
title = StringField(_l('Title'), validators=[DataRequired()])
|
||||||
|
slug = StringField(_l('Slug'), validators=[DataRequired()])
|
||||||
|
body = TextAreaField(_l('Body'), render_kw={'rows': '10'})
|
||||||
|
edit_options = [(0, _l('Mods and admins')),
|
||||||
|
(1, _l('Trusted accounts')),
|
||||||
|
(2, _l('Community members')),
|
||||||
|
(3, _l('Any account'))
|
||||||
|
]
|
||||||
|
who_can_edit = SelectField(_l('Who can edit'), coerce=int, choices=edit_options, validators=[Optional()], render_kw={'class': 'form-select'})
|
||||||
|
submit = SubmitField(_l('Save'))
|
||||||
|
|
||||||
|
|
||||||
class AddModeratorForm(FlaskForm):
|
class AddModeratorForm(FlaskForm):
|
||||||
user_name = StringField(_l('User name'), validators=[DataRequired()])
|
user_name = StringField(_l('User name'), validators=[DataRequired()])
|
||||||
submit = SubmitField(_l('Find'))
|
submit = SubmitField(_l('Find'))
|
||||||
|
|
|
@ -15,7 +15,8 @@ from app.chat.util import send_message
|
||||||
from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, \
|
from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, \
|
||||||
ReportCommunityForm, \
|
ReportCommunityForm, \
|
||||||
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm, \
|
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm, \
|
||||||
EscalateReportForm, ResolveReportForm, CreateVideoForm, CreatePollForm, RetrieveRemotePost
|
EscalateReportForm, ResolveReportForm, CreateVideoForm, CreatePollForm, RetrieveRemotePost, \
|
||||||
|
EditCommunityWikiPageForm
|
||||||
from app.community.util import search_for_community, actor_to_community, \
|
from app.community.util import search_for_community, actor_to_community, \
|
||||||
save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
|
save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
|
||||||
delete_post_from_community, delete_post_reply_from_community, community_in_list, find_local_users
|
delete_post_from_community, delete_post_reply_from_community, community_in_list, find_local_users
|
||||||
|
@ -25,7 +26,8 @@ from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LIN
|
||||||
from app.inoculation import inoculation
|
from app.inoculation import inoculation
|
||||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||||
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply, \
|
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply, \
|
||||||
NotificationSubscription, UserFollower, Instance, Language, Poll, PollChoice, ModLog
|
NotificationSubscription, UserFollower, Instance, Language, Poll, PollChoice, ModLog, CommunityWikiPage, \
|
||||||
|
CommunityWikiPageRevision
|
||||||
from app.community import bp
|
from app.community import bp
|
||||||
from app.user.utils import search_for_user
|
from app.user.utils import search_for_user
|
||||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||||
|
@ -1326,6 +1328,184 @@ def community_moderate_subscribers(actor):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<actor>/moderate/wiki', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def community_wiki_list(actor):
|
||||||
|
community = actor_to_community(actor)
|
||||||
|
|
||||||
|
if community is not None:
|
||||||
|
if community.is_moderator() or current_user.is_admin():
|
||||||
|
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
|
||||||
|
pages = CommunityWikiPage.query.filter(CommunityWikiPage.community_id == community.id).order_by(CommunityWikiPage.title).all()
|
||||||
|
return render_template('community/community_wiki_list.html', title=_('Community Wiki'), community=community,
|
||||||
|
pages=pages, low_bandwidth=low_bandwidth, current='wiki',
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
menu_topics=menu_topics(), site=g.site,
|
||||||
|
inoculation=inoculation[randint(0, len(inoculation) - 1)] if g.site.show_inoculation_block else None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<actor>/moderate/wiki/add', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def community_wiki_add(actor):
|
||||||
|
community = actor_to_community(actor)
|
||||||
|
|
||||||
|
if community is not None:
|
||||||
|
if community.is_moderator() or current_user.is_admin():
|
||||||
|
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
|
||||||
|
form = EditCommunityWikiPageForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
new_page = CommunityWikiPage(community_id=community.id, slug=form.slug.data, title=form.title.data,
|
||||||
|
body=form.body.data, who_can_edit=form.who_can_edit.data)
|
||||||
|
new_page.body_html = markdown_to_html(new_page.body)
|
||||||
|
db.session.add(new_page)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
initial_revision = CommunityWikiPageRevision(wiki_page_id=new_page.id, user_id=current_user.id,
|
||||||
|
community_id=community.id, title=form.title.data,
|
||||||
|
body=form.body.data, body_html=new_page.body_html)
|
||||||
|
db.session.add(initial_revision)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(_('Saved'))
|
||||||
|
return redirect(url_for('community.community_wiki_list', actor=community.link()))
|
||||||
|
|
||||||
|
return render_template('community/community_wiki_edit.html', title=_('Add wiki page'), community=community,
|
||||||
|
form=form, low_bandwidth=low_bandwidth, current='wiki',
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
menu_topics=menu_topics(), site=g.site,
|
||||||
|
inoculation=inoculation[randint(0, len(inoculation) - 1)] if g.site.show_inoculation_block else None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<actor>/wiki/<slug>', methods=['GET', 'POST'])
|
||||||
|
def community_wiki_view(actor, slug):
|
||||||
|
community = actor_to_community(actor)
|
||||||
|
|
||||||
|
if community is not None:
|
||||||
|
page: CommunityWikiPage = CommunityWikiPage.query.filter_by(slug=slug, community_id=community.id).first()
|
||||||
|
if page is None:
|
||||||
|
abort(404)
|
||||||
|
else:
|
||||||
|
# Breadcrumbs
|
||||||
|
breadcrumbs = []
|
||||||
|
breadcrumb = namedtuple("Breadcrumb", ['text', 'url'])
|
||||||
|
breadcrumb.text = _('Home')
|
||||||
|
breadcrumb.url = '/'
|
||||||
|
breadcrumbs.append(breadcrumb)
|
||||||
|
|
||||||
|
if community.topic_id:
|
||||||
|
topics = []
|
||||||
|
previous_topic = Topic.query.get(community.topic_id)
|
||||||
|
topics.append(previous_topic)
|
||||||
|
while previous_topic.parent_id:
|
||||||
|
topic = Topic.query.get(previous_topic.parent_id)
|
||||||
|
topics.append(topic)
|
||||||
|
previous_topic = topic
|
||||||
|
topics = list(reversed(topics))
|
||||||
|
|
||||||
|
breadcrumb = namedtuple("Breadcrumb", ['text', 'url'])
|
||||||
|
breadcrumb.text = _('Topics')
|
||||||
|
breadcrumb.url = '/topics'
|
||||||
|
breadcrumbs.append(breadcrumb)
|
||||||
|
|
||||||
|
existing_url = '/topic'
|
||||||
|
for topic in topics:
|
||||||
|
breadcrumb = namedtuple("Breadcrumb", ['text', 'url'])
|
||||||
|
breadcrumb.text = topic.name
|
||||||
|
breadcrumb.url = f"{existing_url}/{topic.machine_name}"
|
||||||
|
breadcrumbs.append(breadcrumb)
|
||||||
|
existing_url = breadcrumb.url
|
||||||
|
else:
|
||||||
|
breadcrumb = namedtuple("Breadcrumb", ['text', 'url'])
|
||||||
|
breadcrumb.text = _('Communities')
|
||||||
|
breadcrumb.url = '/communities'
|
||||||
|
breadcrumbs.append(breadcrumb)
|
||||||
|
|
||||||
|
return render_template('community/community_wiki_page_view.html', title=page.title, page=page,
|
||||||
|
community=community, breadcrumbs=breadcrumbs, is_moderator=community.is_moderator(),
|
||||||
|
is_owner=community.is_owner(),
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
menu_topics=menu_topics(), site=g.site,
|
||||||
|
inoculation=inoculation[
|
||||||
|
randint(0, len(inoculation) - 1)] if g.site.show_inoculation_block else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<actor>/moderate/wiki/<int:page_id>/edit', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def community_wiki_edit(actor, page_id):
|
||||||
|
community = actor_to_community(actor)
|
||||||
|
|
||||||
|
if community is not None:
|
||||||
|
page: CommunityWikiPage = CommunityWikiPage.query.get_or_404(page_id)
|
||||||
|
if page.can_edit(current_user, community):
|
||||||
|
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
|
||||||
|
|
||||||
|
form = EditCommunityWikiPageForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
page.title = form.title.data
|
||||||
|
page.slug = form.slug.data
|
||||||
|
page.body = form.body.data
|
||||||
|
page.body_html = markdown_to_html(page.body)
|
||||||
|
page.who_can_edit = form.who_can_edit.data
|
||||||
|
page.edited_at = utcnow()
|
||||||
|
new_revision = CommunityWikiPageRevision(wiki_page_id=page.id, user_id=current_user.id,
|
||||||
|
community_id=community.id, title=form.title.data,
|
||||||
|
body=form.body.data, body_html=page.body_html)
|
||||||
|
db.session.add(new_revision)
|
||||||
|
db.session.commit()
|
||||||
|
flash(_('Saved'))
|
||||||
|
if request.args.get('return') == 'list':
|
||||||
|
return redirect(url_for('community.community_wiki_list', actor=community.link()))
|
||||||
|
elif request.args.get('return') == 'page':
|
||||||
|
return redirect(url_for('community.community_wiki_view', actor=community.link(), slug=page.slug))
|
||||||
|
else:
|
||||||
|
form.title.data = page.title
|
||||||
|
form.slug.data = page.slug
|
||||||
|
form.body.data = page.body
|
||||||
|
form.who_can_edit.data = page.who_can_edit
|
||||||
|
|
||||||
|
return render_template('community/community_wiki_edit.html', title=_('Edit wiki page'), community=community,
|
||||||
|
form=form, low_bandwidth=low_bandwidth,
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
menu_topics=menu_topics(), site=g.site,
|
||||||
|
inoculation=inoculation[randint(0, len(inoculation) - 1)] if g.site.show_inoculation_block else None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<actor>/moderate/wiki/<int:page_id>/delete', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def community_wiki_delete(actor, page_id):
|
||||||
|
community = actor_to_community(actor)
|
||||||
|
|
||||||
|
if community is not None:
|
||||||
|
page: CommunityWikiPage = CommunityWikiPage.query.get_or_404(page_id)
|
||||||
|
if page.can_edit(current_user, community):
|
||||||
|
db.session.delete(page)
|
||||||
|
db.session.commit()
|
||||||
|
flash(_('Page deleted'))
|
||||||
|
return redirect(url_for('community.community_wiki_list', actor=community.link()))
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<actor>/moderate/modlog', methods=['GET'])
|
@bp.route('/<actor>/moderate/modlog', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def community_modlog(actor):
|
def community_modlog(actor):
|
||||||
|
|
|
@ -406,6 +406,7 @@ class Community(db.Model):
|
||||||
|
|
||||||
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
|
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
|
||||||
replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
|
replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
|
||||||
|
wiki_pages = db.relationship('CommunityWikiPage', lazy='dynamic', backref='community', cascade="all, delete-orphan")
|
||||||
icon = db.relationship('File', foreign_keys=[icon_id], single_parent=True, backref='community', cascade="all, delete-orphan")
|
icon = db.relationship('File', foreign_keys=[icon_id], single_parent=True, backref='community', cascade="all, delete-orphan")
|
||||||
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
|
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
|
||||||
languages = db.relationship('Language', lazy='dynamic', secondary=community_language, backref=db.backref('communities', lazy='dynamic'))
|
languages = db.relationship('Language', lazy='dynamic', secondary=community_language, backref=db.backref('communities', lazy='dynamic'))
|
||||||
|
@ -476,15 +477,25 @@ class Community(db.Model):
|
||||||
))
|
))
|
||||||
).filter(CommunityMember.is_banned == False).all()
|
).filter(CommunityMember.is_banned == False).all()
|
||||||
|
|
||||||
|
def is_member(self, user):
|
||||||
|
if user is None:
|
||||||
|
return CommunityMember.query.filter(CommunityMember.user_id == current_user.get_id(),
|
||||||
|
CommunityMember.community_id == self.id,
|
||||||
|
CommunityMember.is_banned == False).all()
|
||||||
|
else:
|
||||||
|
return CommunityMember.query.filter(CommunityMember.user_id == user.id,
|
||||||
|
CommunityMember.community_id == self.id,
|
||||||
|
CommunityMember.is_banned == False).all()
|
||||||
|
|
||||||
def is_moderator(self, user=None):
|
def is_moderator(self, user=None):
|
||||||
if user is None:
|
if user is None:
|
||||||
return any(moderator.user_id == current_user.id for moderator in self.moderators())
|
return any(moderator.user_id == current_user.get_id() for moderator in self.moderators())
|
||||||
else:
|
else:
|
||||||
return any(moderator.user_id == user.id for moderator in self.moderators())
|
return any(moderator.user_id == user.id for moderator in self.moderators())
|
||||||
|
|
||||||
def is_owner(self, user=None):
|
def is_owner(self, user=None):
|
||||||
if user is None:
|
if user is None:
|
||||||
return any(moderator.user_id == current_user.id and moderator.is_owner for moderator in self.moderators())
|
return any(moderator.user_id == current_user.get_id() and moderator.is_owner for moderator in self.moderators())
|
||||||
else:
|
else:
|
||||||
return any(moderator.user_id == user.id and moderator.is_owner for moderator in self.moderators())
|
return any(moderator.user_id == user.id and moderator.is_owner for moderator in self.moderators())
|
||||||
|
|
||||||
|
@ -1246,6 +1257,46 @@ class CommunityMember(db.Model):
|
||||||
created_at = db.Column(db.DateTime, default=utcnow)
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class CommunityWikiPage(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), index=True)
|
||||||
|
slug = db.Column(db.String(100), index=True)
|
||||||
|
title = db.Column(db.String(255))
|
||||||
|
body = db.Column(db.Text)
|
||||||
|
body_html = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
edited_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
who_can_edit = db.Column(db.Integer, default=0) # 0 = mods & admins, 1 = trusted, 2 = community members, 3 = anyone
|
||||||
|
revisions = db.relationship('CommunityWikiPageRevision', backref=db.backref('page'), cascade='all,delete',
|
||||||
|
lazy='dynamic')
|
||||||
|
def can_edit(self, user: User, community: Community):
|
||||||
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
if self.who_can_edit == 0:
|
||||||
|
if user.is_admin() or user.is_staff() or community.is_moderator(user):
|
||||||
|
return True
|
||||||
|
elif self.who_can_edit == 1:
|
||||||
|
if user.is_admin() or user.is_staff() or community.is_moderator(user) or user.trustworthy():
|
||||||
|
return True
|
||||||
|
elif self.who_can_edit == 2:
|
||||||
|
if user.is_admin() or user.is_staff() or community.is_moderator(user) or user.trustworthy() or community.is_member(user):
|
||||||
|
return True
|
||||||
|
elif self.who_can_edit == 3:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class CommunityWikiPageRevision(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
wiki_page_id = db.Column(db.Integer, db.ForeignKey('community_wiki_page.id'), index=True)
|
||||||
|
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), index=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
|
title = db.Column(db.String(255))
|
||||||
|
body = db.Column(db.Text)
|
||||||
|
body_html = db.Column(db.Text)
|
||||||
|
edited_at = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
||||||
|
|
||||||
class UserFollower(db.Model):
|
class UserFollower(db.Model):
|
||||||
local_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
local_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||||
remote_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
remote_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||||
|
|
|
@ -55,7 +55,7 @@ def show_post(post_id: int):
|
||||||
flash(_('%(name)s has indicated they made a mistake in this post.', name=post.author.user_name), 'warning')
|
flash(_('%(name)s has indicated they made a mistake in this post.', name=post.author.user_name), 'warning')
|
||||||
|
|
||||||
mods = community_moderators(community.id)
|
mods = community_moderators(community.id)
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
is_moderator = community.is_moderator()
|
||||||
|
|
||||||
if community.private_mods:
|
if community.private_mods:
|
||||||
mod_list = []
|
mod_list = []
|
||||||
|
@ -310,21 +310,19 @@ def show_post(post_id: int):
|
||||||
if post.type == POST_TYPE_LINK and body_has_no_archive_link(post.body_html) and url_needs_archive(post.url):
|
if post.type == POST_TYPE_LINK and body_has_no_archive_link(post.body_html) and url_needs_archive(post.url):
|
||||||
archive_link = generate_archive_link(post.url)
|
archive_link = generate_archive_link(post.url)
|
||||||
|
|
||||||
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, is_owner=community.is_owner(),
|
||||||
|
community=post.community,
|
||||||
breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list,
|
breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list,
|
||||||
poll_form=poll_form, poll_results=poll_results, poll_data=poll_data, poll_choices=poll_choices, poll_total_votes=poll_total_votes,
|
poll_form=poll_form, poll_results=poll_results, poll_data=poll_data, poll_choices=poll_choices, poll_total_votes=poll_total_votes,
|
||||||
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
||||||
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
|
description=description, og_image=og_image,
|
||||||
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE,
|
|
||||||
POST_TYPE_VIDEO=constants.POST_TYPE_VIDEO, POST_TYPE_POLL=constants.POST_TYPE_POLL,
|
|
||||||
autoplay=request.args.get('autoplay', False), archive_link=archive_link,
|
autoplay=request.args.get('autoplay', False), archive_link=archive_link,
|
||||||
noindex=not post.author.indexable, preconnect=post.url if post.url else None,
|
noindex=not post.author.indexable, preconnect=post.url if post.url else None,
|
||||||
recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted,
|
recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted,
|
||||||
recently_upvoted_replies=recently_upvoted_replies, recently_downvoted_replies=recently_downvoted_replies,
|
recently_upvoted_replies=recently_upvoted_replies, recently_downvoted_replies=recently_downvoted_replies,
|
||||||
reply_collapse_threshold=reply_collapse_threshold,
|
reply_collapse_threshold=reply_collapse_threshold,
|
||||||
etag=f"{post.id}{sort}_{hash(post.last_active)}", markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
|
etag=f"{post.id}{sort}_{hash(post.last_active)}", markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
|
||||||
low_bandwidth=request.cookies.get('low_bandwidth', '0') == '1', SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
low_bandwidth=request.cookies.get('low_bandwidth', '0') == '1',
|
||||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
|
||||||
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()),
|
||||||
menu_topics=menu_topics(), site=g.site,
|
menu_topics=menu_topics(), site=g.site,
|
||||||
|
|
|
@ -13,6 +13,9 @@
|
||||||
<a href="{{ url_for('community.community_moderate_subscribers', actor=community.link()) }}" class="btn {{ 'btn-primary' if current == 'subscribers' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
<a href="{{ url_for('community.community_moderate_subscribers', actor=community.link()) }}" class="btn {{ 'btn-primary' if current == 'subscribers' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
{{ _('Subscribers') }}
|
{{ _('Subscribers') }}
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('community.community_wiki_list', actor=community.link()) }}" class="btn {{ 'btn-primary' if current == 'wiki' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
|
{{ _('Wiki') }}
|
||||||
|
</a>
|
||||||
<a href="/community/{{ community.link() }}/moderate/appeals" class="btn {{ 'btn-primary' if current == 'appeals' else 'btn-outline-secondary' }} disabled" rel="nofollow noindex" >
|
<a href="/community/{{ community.link() }}/moderate/appeals" class="btn {{ 'btn-primary' if current == 'appeals' else 'btn-outline-secondary' }} disabled" rel="nofollow noindex" >
|
||||||
{{ _('Appeals') }}
|
{{ _('Appeals') }}
|
||||||
</a>
|
</a>
|
||||||
|
|
18
app/templates/community/community_wiki_edit.html
Normal file
18
app/templates/community/community_wiki_edit.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% 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, render_field %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
{% block title %}<h1>{{ title }}</h1>{% endblock %}
|
||||||
|
{{ render_form(form) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "_side_pane.html" %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
67
app/templates/community/community_wiki_list.html
Normal file
67
app/templates/community/community_wiki_list.html
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
{% 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_field %}
|
||||||
|
|
||||||
|
{% 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="{{ url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not none else community.name) }}">{{ (community.title + '@' + community.ap_domain)|shorten }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ url_for('community.community_edit', community_id=community.id) }}">{{ _('Settings') }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ _('Wiki') }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{% include "community/_community_moderation_nav.html" %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-10">
|
||||||
|
<h1 class="mt-2">{{ _('Wiki pages for %(community)s', community=community.display_name()) }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
<a class="btn btn-primary" href="{{ url_for('community.community_wiki_add', actor=community.link()) }}">{{ _('Add wiki page') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if pages -%}
|
||||||
|
<table class="table table-responsive">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ _('Name') }}</th>
|
||||||
|
<th>{{ _('Url') }}</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for page in pages %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ page.title }}</td>
|
||||||
|
<td><a href="{{ url_for('community.community_wiki_view', actor=community.link(), slug=page.slug) }}">{{ page.slug }}</a></td>
|
||||||
|
<td class="text-right">{% if page.can_edit(current_user, community) %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Actions
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('community.community_wiki_view', actor=community.link(), slug=page.slug) }}">{{ _('View') }}</a></li>
|
||||||
|
<li><a class="dropdown-item"
|
||||||
|
href="{{ url_for('community.community_wiki_edit', actor=community.link(), page_id=page.id, return='list') }}">
|
||||||
|
{{ _('Edit') }}</a></li>
|
||||||
|
<li><a class="confirm_first dropdown-item"
|
||||||
|
href="{{ url_for('community.community_wiki_delete', actor=community.link(), page_id=page.id) }}">
|
||||||
|
{{ _('Delete') }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else -%}
|
||||||
|
<p>{{ _('There are no wiki pages in this community.') }}</p>
|
||||||
|
{% endif -%}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
33
app/templates/community/community_wiki_page_view.html
Normal file
33
app/templates/community/community_wiki_page_view.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% 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">
|
||||||
|
<div class="row position-relative">
|
||||||
|
<div class="col post_col post_type_normal">
|
||||||
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
{% for breadcrumb in breadcrumbs -%}
|
||||||
|
<li class="breadcrumb-item">{% if breadcrumb.url -%}<a href="{{ breadcrumb.url }}">{% endif -%}{{ breadcrumb.text }}{% if breadcrumb.url -%}</a>{% endif -%}</li>
|
||||||
|
{% endfor -%}
|
||||||
|
<li class="breadcrumb-item"><a href="/c/{{ page.community.link() }}">{{ page.community.title }}@{{ page.community.ap_domain }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ page.title|shorten(15) }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h1 class="mt-2 post_title">{{ page.title }}</h1>
|
||||||
|
{{ page.body_html | safe }}
|
||||||
|
{% if page.can_edit(current_user, community) -%}
|
||||||
|
<p><a class="btn btn-primary" href="{{ url_for('community.community_wiki_edit', actor=community.link(), page_id=page.id, return='page') }}">{{ _('Edit') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include "_side_pane.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock -%}
|
71
migrations/versions/b97584a7a10b_wikis.py
Normal file
71
migrations/versions/b97584a7a10b_wikis.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
"""wikis
|
||||||
|
|
||||||
|
Revision ID: b97584a7a10b
|
||||||
|
Revises: ea5a07acf23c
|
||||||
|
Create Date: 2024-07-17 20:54:20.626562
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b97584a7a10b'
|
||||||
|
down_revision = 'ea5a07acf23c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('community_wiki_page',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('community_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('slug', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('title', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('body', sa.Text(), nullable=True),
|
||||||
|
sa.Column('body_html', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('edited_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('who_can_edit', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['community_id'], ['community.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('community_wiki_page', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_community_wiki_page_community_id'), ['community_id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_community_wiki_page_slug'), ['slug'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('community_wiki_page_revision',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('wiki_page_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('community_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('title', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('body', sa.Text(), nullable=True),
|
||||||
|
sa.Column('body_html', sa.Text(), nullable=True),
|
||||||
|
sa.Column('edited_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['community_id'], ['community.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['wiki_page_id'], ['community_wiki_page.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('community_wiki_page_revision', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_community_wiki_page_revision_community_id'), ['community_id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_community_wiki_page_revision_wiki_page_id'), ['wiki_page_id'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('community_wiki_page_revision', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_community_wiki_page_revision_wiki_page_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_community_wiki_page_revision_community_id'))
|
||||||
|
|
||||||
|
op.drop_table('community_wiki_page_revision')
|
||||||
|
with op.batch_alter_table('community_wiki_page', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_community_wiki_page_slug'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_community_wiki_page_community_id'))
|
||||||
|
|
||||||
|
op.drop_table('community_wiki_page')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -9,7 +9,7 @@ from app import create_app, db, cli
|
||||||
import os, click
|
import os, click
|
||||||
from flask import session, g, json, request, current_app
|
from flask import session, g, json, request, current_app
|
||||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE, POST_TYPE_VIDEO, POST_TYPE_POLL, \
|
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE, POST_TYPE_VIDEO, POST_TYPE_POLL, \
|
||||||
SUBSCRIPTION_MODERATOR
|
SUBSCRIPTION_MODERATOR, SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_PENDING
|
||||||
from app.models import Site
|
from app.models import Site
|
||||||
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership, \
|
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership, \
|
||||||
can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href, \
|
can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href, \
|
||||||
|
@ -26,7 +26,8 @@ def app_context_processor():
|
||||||
return dict(getmtime=getmtime, instance_domain=current_app.config['SERVER_NAME'],
|
return dict(getmtime=getmtime, instance_domain=current_app.config['SERVER_NAME'],
|
||||||
POST_TYPE_LINK=POST_TYPE_LINK, POST_TYPE_IMAGE=POST_TYPE_IMAGE,
|
POST_TYPE_LINK=POST_TYPE_LINK, POST_TYPE_IMAGE=POST_TYPE_IMAGE,
|
||||||
POST_TYPE_ARTICLE=POST_TYPE_ARTICLE, POST_TYPE_VIDEO=POST_TYPE_VIDEO, POST_TYPE_POLL=POST_TYPE_POLL,
|
POST_TYPE_ARTICLE=POST_TYPE_ARTICLE, POST_TYPE_VIDEO=POST_TYPE_VIDEO, POST_TYPE_POLL=POST_TYPE_POLL,
|
||||||
SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR)
|
SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||||
|
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING)
|
||||||
|
|
||||||
|
|
||||||
@app.shell_context_processor
|
@app.shell_context_processor
|
||||||
|
|
Loading…
Reference in a new issue