mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
community wiki - revisions #127
This commit is contained in:
parent
2c3f1763b3
commit
82cc9389ba
6 changed files with 215 additions and 7 deletions
|
@ -848,9 +848,9 @@ def community_edit(community_id: int):
|
|||
if form.validate_on_submit():
|
||||
community.title = form.title.data
|
||||
community.description = form.description.data
|
||||
community.description_html = markdown_to_html(form.description.data)
|
||||
community.description_html = markdown_to_html(form.description.data, anchors_new_tab=False)
|
||||
community.rules = form.rules.data
|
||||
community.rules_html = markdown_to_html(form.rules.data)
|
||||
community.rules_html = markdown_to_html(form.rules.data, anchors_new_tab=False)
|
||||
community.nsfw = form.nsfw.data
|
||||
community.local_only = form.local_only.data
|
||||
community.restricted_to_mods = form.restricted_to_mods.data
|
||||
|
@ -1443,6 +1443,91 @@ def community_wiki_view(actor, slug):
|
|||
)
|
||||
|
||||
|
||||
@bp.route('/<actor>/wiki/<slug>/<revision_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def community_wiki_view_revision(actor, slug, revision_id):
|
||||
community = actor_to_community(actor)
|
||||
|
||||
if community is not None:
|
||||
page: CommunityWikiPage = CommunityWikiPage.query.filter_by(slug=slug, community_id=community.id).first()
|
||||
revision: CommunityWikiPageRevision = CommunityWikiPageRevision.query.get_or_404(revision_id)
|
||||
if page is None or revision 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_revision_view.html', title=page.title, page=page,
|
||||
community=community, breadcrumbs=breadcrumbs, is_moderator=community.is_moderator(),
|
||||
is_owner=community.is_owner(), revision=revision,
|
||||
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>/wiki/<slug>/<revision_id>/revert', methods=['GET'])
|
||||
@login_required
|
||||
def community_wiki_revert_revision(actor, slug, revision_id):
|
||||
community = actor_to_community(actor)
|
||||
|
||||
if community is not None:
|
||||
page: CommunityWikiPage = CommunityWikiPage.query.filter_by(slug=slug, community_id=community.id).first()
|
||||
revision: CommunityWikiPageRevision = CommunityWikiPageRevision.query.get_or_404(revision_id)
|
||||
if page is None or revision is None:
|
||||
abort(404)
|
||||
else:
|
||||
if page.can_edit(current_user, community):
|
||||
page.body = revision.body
|
||||
page.body_html = revision.body_html
|
||||
page.edited_at = utcnow()
|
||||
|
||||
new_revision = CommunityWikiPageRevision(wiki_page_id=page.id, user_id=current_user.id,
|
||||
community_id=community.id, title=revision.title,
|
||||
body=revision.body, body_html=revision.body_html)
|
||||
db.session.add(new_revision)
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Reverted to old version of the page.'))
|
||||
return redirect(url_for('community.community_wiki_revisions', actor=community.link(), page_id=page.id))
|
||||
else:
|
||||
abort(401)
|
||||
|
||||
|
||||
@bp.route('/<actor>/moderate/wiki/<int:page_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def community_wiki_edit(actor, page_id):
|
||||
|
@ -1490,6 +1575,35 @@ def community_wiki_edit(actor, page_id):
|
|||
abort(404)
|
||||
|
||||
|
||||
@bp.route('/<actor>/moderate/wiki/<int:page_id>/revisions', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def community_wiki_revisions(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'
|
||||
|
||||
revisions = CommunityWikiPageRevision.query.filter_by(wiki_page_id=page.id).\
|
||||
order_by(desc(CommunityWikiPageRevision.edited_at)).all()
|
||||
|
||||
most_recent_revision = revisions[0].id
|
||||
|
||||
return render_template('community/community_wiki_revisions.html', title=_('%(title)s revisions', title=page.title),
|
||||
community=community, page=page, revisions=revisions, most_recent_revision=most_recent_revision,
|
||||
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):
|
||||
|
|
|
@ -1296,6 +1296,8 @@ class CommunityWikiPageRevision(db.Model):
|
|||
body_html = db.Column(db.Text)
|
||||
edited_at = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
author = db.relationship('User', lazy='joined', foreign_keys=[user_id])
|
||||
|
||||
|
||||
class UserFollower(db.Model):
|
||||
local_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
|
|
|
@ -45,7 +45,8 @@
|
|||
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_view', actor=community.link(), slug=page.slug) }}">{{ _('View page') }}</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('community.community_wiki_revisions', actor=community.link(), page_id=page.id) }}">{{ _('View revisions') }}</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>
|
||||
|
@ -59,6 +60,7 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>{{ _('Add a link to the wiki in the community description.') }}</p>
|
||||
{% else -%}
|
||||
<p>{{ _('There are no wiki pages in this community.') }}</p>
|
||||
{% endif -%}
|
||||
|
|
30
app/templates/community/community_wiki_revision_view.html
Normal file
30
app/templates/community/community_wiki_revision_view.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% 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">{{ revision.title }}</h1>
|
||||
{{ revision.body_html | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "_side_pane.html" %}
|
||||
</div>
|
||||
{% endblock -%}
|
60
app/templates/community/community_wiki_revisions.html
Normal file
60
app/templates/community/community_wiki_revisions.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
{% 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">{{ _('Revisions of %(title)s', title=page.title) }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{% if revisions -%}
|
||||
<table class="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _('Author') }}</th>
|
||||
<th>{{ _('When') }}</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for revision in revisions %}
|
||||
<tr>
|
||||
<td>{{ render_username(revision.author) }}</td>
|
||||
<td>{{ moment(revision.edited_at).fromNow() }}</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_revision', actor=community.link(), slug=page.slug, revision_id=revision.id) }}">{{ _('View') }}</a></li>
|
||||
{% if revision.id != most_recent_revision %}<li><a class="dropdown-item confirm_first"
|
||||
href="{{ url_for('community.community_wiki_revert_revision', actor=community.link(), slug=page.slug, revision_id=revision.id, return='list') }}">
|
||||
{{ _('Revert') }}</a></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -216,7 +216,7 @@ def mime_type_using_head(url):
|
|||
|
||||
|
||||
# sanitise HTML using an allow list
|
||||
def allowlist_html(html: str) -> str:
|
||||
def allowlist_html(html: str, a_target='_blank') -> str:
|
||||
if html is None or html == '':
|
||||
return ''
|
||||
allowed_tags = ['p', 'strong', 'a', 'ul', 'ol', 'li', 'em', 'blockquote', 'cite', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre',
|
||||
|
@ -262,7 +262,7 @@ def allowlist_html(html: str) -> str:
|
|||
# Add nofollow and target=_blank to anchors
|
||||
if tag.name == 'a':
|
||||
tag.attrs['rel'] = 'nofollow ugc'
|
||||
tag.attrs['target'] = '_blank'
|
||||
tag.attrs['target'] = a_target
|
||||
# Add loading=lazy to images
|
||||
if tag.name == 'img':
|
||||
tag.attrs['loading'] = 'lazy'
|
||||
|
@ -275,14 +275,14 @@ def allowlist_html(html: str) -> str:
|
|||
|
||||
|
||||
# this is for pyfedi's version of Markdown (differs from lemmy for: newlines for soft breaks, ...)
|
||||
def markdown_to_html(markdown_text) -> str:
|
||||
def markdown_to_html(markdown_text, anchors_new_tab=True) -> str:
|
||||
if markdown_text:
|
||||
raw_html = markdown2.markdown(markdown_text, safe_mode=True,
|
||||
extras={'middle-word-em': False, 'tables': True, 'fenced-code-blocks': True, 'strike': True, 'breaks': {'on_newline': True, 'on_backslash': True}})
|
||||
# support lemmy's spoiler format
|
||||
re_spoiler = re.compile(r':{3}\s*?spoiler\s+?(\S.+?)(?:\n|</p>)(.+?)(?:\n|<p>):{3}', re.S)
|
||||
raw_html = re_spoiler.sub(r'<details><summary>\1</summary><p>\2</p></details>', raw_html)
|
||||
return allowlist_html(raw_html)
|
||||
return allowlist_html(raw_html, a_target='_blank' if anchors_new_tab else '')
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
|
Loading…
Reference in a new issue