community wiki - revisions #127

This commit is contained in:
rimu 2024-07-18 15:14:55 +08:00
parent 2c3f1763b3
commit 82cc9389ba
6 changed files with 215 additions and 7 deletions

View file

@ -848,9 +848,9 @@ def community_edit(community_id: int):
if form.validate_on_submit(): if form.validate_on_submit():
community.title = form.title.data community.title = form.title.data
community.description = form.description.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 = 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.nsfw = form.nsfw.data
community.local_only = form.local_only.data community.local_only = form.local_only.data
community.restricted_to_mods = form.restricted_to_mods.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']) @bp.route('/<actor>/moderate/wiki/<int:page_id>/edit', methods=['GET', 'POST'])
@login_required @login_required
def community_wiki_edit(actor, page_id): def community_wiki_edit(actor, page_id):
@ -1490,6 +1575,35 @@ def community_wiki_edit(actor, page_id):
abort(404) 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']) @bp.route('/<actor>/moderate/wiki/<int:page_id>/delete', methods=['GET'])
@login_required @login_required
def community_wiki_delete(actor, page_id): def community_wiki_delete(actor, page_id):

View file

@ -1296,6 +1296,8 @@ class CommunityWikiPageRevision(db.Model):
body_html = db.Column(db.Text) body_html = db.Column(db.Text)
edited_at = db.Column(db.DateTime, default=utcnow) edited_at = db.Column(db.DateTime, default=utcnow)
author = db.relationship('User', lazy='joined', foreign_keys=[user_id])
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)

View file

@ -45,7 +45,8 @@
Actions Actions
</button> </button>
<ul class="dropdown-menu"> <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" <li><a class="dropdown-item"
href="{{ url_for('community.community_wiki_edit', actor=community.link(), page_id=page.id, return='list') }}"> href="{{ url_for('community.community_wiki_edit', actor=community.link(), page_id=page.id, return='list') }}">
{{ _('Edit') }}</a></li> {{ _('Edit') }}</a></li>
@ -59,6 +60,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<p>{{ _('Add a link to the wiki in the community description.') }}</p>
{% else -%} {% else -%}
<p>{{ _('There are no wiki pages in this community.') }}</p> <p>{{ _('There are no wiki pages in this community.') }}</p>
{% endif -%} {% endif -%}

View 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 -%}

View 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 %}

View file

@ -216,7 +216,7 @@ def mime_type_using_head(url):
# sanitise HTML using an allow list # 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 == '': if html is None or html == '':
return '' return ''
allowed_tags = ['p', 'strong', 'a', 'ul', 'ol', 'li', 'em', 'blockquote', 'cite', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 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 # Add nofollow and target=_blank to anchors
if tag.name == 'a': if tag.name == 'a':
tag.attrs['rel'] = 'nofollow ugc' tag.attrs['rel'] = 'nofollow ugc'
tag.attrs['target'] = '_blank' tag.attrs['target'] = a_target
# Add loading=lazy to images # Add loading=lazy to images
if tag.name == 'img': if tag.name == 'img':
tag.attrs['loading'] = 'lazy' 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, ...) # 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: if markdown_text:
raw_html = markdown2.markdown(markdown_text, safe_mode=True, 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}}) 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 # support lemmy's spoiler format
re_spoiler = re.compile(r':{3}\s*?spoiler\s+?(\S.+?)(?:\n|</p>)(.+?)(?:\n|<p>):{3}', re.S) 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) 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: else:
return '' return ''