Merge pull request #409 from mouse-reeve/html-fields

Allow markdown in html fields
This commit is contained in:
Mouse Reeve 2020-12-19 20:32:53 -08:00 committed by GitHub
commit a771b1d5b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 48 additions and 28 deletions

View file

@ -11,6 +11,7 @@ from django.core.files.base import ContentFile
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from markdown import markdown
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.sanitize_html import InputHtmlParser
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
@ -25,6 +26,7 @@ def validate_remote_id(value):
params={'value': value}, params={'value': value},
) )
def validate_username(value): def validate_username(value):
''' make sure usernames look okay ''' ''' make sure usernames look okay '''
if not re.match(r'^[A-Za-z\-_\.]+$', value): if not re.match(r'^[A-Za-z\-_\.]+$', value):
@ -399,6 +401,16 @@ class HtmlField(ActivitypubFieldMixin, models.TextField):
sanitizer.feed(value) sanitizer.feed(value)
return sanitizer.get_output() return sanitizer.get_output()
def to_python(self, value):# pylint: disable=no-self-use
''' process markdown before save '''
if not value:
return value
content = markdown(value)
# sanitize resulting html
sanitizer = InputHtmlParser()
sanitizer.feed(content)
return sanitizer.get_output()
class ArrayField(ActivitypubFieldMixin, DjangoArrayField): class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
''' activitypub-aware array field ''' ''' activitypub-aware array field '''
def field_to_activity(self, value): def field_to_activity(self, value):

View file

@ -211,18 +211,16 @@ def handle_status(user, form):
''' generic handler for statuses ''' ''' generic handler for statuses '''
status = form.save(commit=False) status = form.save(commit=False)
if not status.sensitive and status.content_warning: if not status.sensitive and status.content_warning:
# the cw text field remains populated hen you click "remove" # the cw text field remains populated when you click "remove"
status.content_warning = None status.content_warning = None
status.save() status.save()
# inspect the text for user tags # inspect the text for user tags
text = status.content matches = []
matches = re.finditer( for match in re.finditer(regex.username, status.content):
regex.username,
text
)
for match in matches:
username = match.group().strip().split('@')[1:] username = match.group().strip().split('@')[1:]
print(match.group())
print(len(username))
if len(username) == 1: if len(username) == 1:
# this looks like a local user (@user), fill in the domain # this looks like a local user (@user), fill in the domain
username.append(DOMAIN) username.append(DOMAIN)
@ -232,6 +230,7 @@ def handle_status(user, form):
if not mention_user: if not mention_user:
# we can ignore users we don't know about # we can ignore users we don't know about
continue continue
matches.append((match.group(), mention_user.remote_id))
# add them to status mentions fk # add them to status mentions fk
status.mention_users.add(mention_user) status.mention_users.add(mention_user)
# create notification if the mentioned user is local # create notification if the mentioned user is local
@ -242,6 +241,20 @@ def handle_status(user, form):
related_user=user, related_user=user,
related_status=status related_status=status
) )
# add links
content = status.content
content = re.sub(
r'([^(href=")])(https?:\/\/([A-Za-z\.\-_\/]+' \
r'\.[A-Za-z]{2,}[A-Za-z\.\-_\/]+))',
r'\g<1><a href="\g<2>">\g<3></a>',
content)
for (username, url) in matches:
content = re.sub(
r'%s([^@])' % username,
r'<a href="%s">%s</a>\g<1>' % (url, username),
content)
status.content = content
status.save() status.save()
# notify reply parent or tagged users # notify reply parent or tagged users

View file

@ -6,7 +6,11 @@ class InputHtmlParser(HTMLParser):#pylint: disable=abstract-method
def __init__(self): def __init__(self):
HTMLParser.__init__(self) HTMLParser.__init__(self)
self.allowed_tags = ['p', 'b', 'i', 'pre', 'a', 'span'] self.allowed_tags = [
'p', 'br',
'b', 'i', 'strong', 'em', 'pre',
'a', 'span', 'ul', 'ol', 'li'
]
self.tag_stack = [] self.tag_stack = []
self.output = [] self.output = []
# if the html appears invalid, we just won't allow any at all # if the html appears invalid, we just won't allow any at all

View file

@ -137,8 +137,3 @@ input.toggle-control:checked ~ .modal.toggle-content {
content: "\e904"; content: "\e904";
right: 0; right: 0;
} }
/* --- BLOCKQUOTE --- */
blockquote {
white-space: pre-line;
}

View file

@ -103,14 +103,14 @@
<div> <div>
{% for shelf in user_shelves %} {% for shelf in user_shelves %}
<p> <p>
This edition is on your <a href="/user/{{ user.localname }}/shelves/{{ shelf.shelf.identifier }}">{{ shelf.shelf.name }}</a> shelf. This edition is on your <a href="/user/{{ user.localname }}/shelf/{{ shelf.shelf.identifier }}">{{ shelf.shelf.name }}</a> shelf.
{% include 'snippets/shelf_selector.html' with current=shelf.shelf %} {% include 'snippets/shelf_selector.html' with current=shelf.shelf %}
</p> </p>
{% endfor %} {% endfor %}
{% for shelf in other_edition_shelves %} {% for shelf in other_edition_shelves %}
<p> <p>
A <a href="/book/{{ shelf.book.id }}">different edition</a> of this book is on your <a href="/user/{{ user.localname }}/shelves/{{ shelf.shelf.identifier }}">{{ shelf.shelf.name }}</a> shelf. A <a href="/book/{{ shelf.book.id }}">different edition</a> of this book is on your <a href="/user/{{ user.localname }}/shelf/{{ shelf.shelf.identifier }}">{{ shelf.shelf.name }}</a> shelf.
{% include 'snippets/switch_edition_button.html' with edition=book %} {% include 'snippets/switch_edition_button.html' with edition=book %}
</p> </p>
{% endfor %} {% endfor %}

View file

@ -27,7 +27,7 @@
{% if status.quote %} {% if status.quote %}
<div class="quote block"> <div class="quote block">
<blockquote>{{ status.quote }}</blockquote> <blockquote>{{ status.quote | safe }}</blockquote>
<p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p> <p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p>
</div> </div>

View file

@ -262,8 +262,8 @@ class Incoming(TestCase):
status = models.Quotation.objects.get() status = models.Quotation.objects.get()
self.assertEqual( self.assertEqual(
status.remote_id, 'https://example.com/user/mouse/quotation/13') status.remote_id, 'https://example.com/user/mouse/quotation/13')
self.assertEqual(status.quote, 'quote body') self.assertEqual(status.quote, '<p>quote body</p>')
self.assertEqual(status.content, 'commentary') self.assertEqual(status.content, '<p>commentary</p>')
self.assertEqual(status.user, self.local_user) self.assertEqual(status.user, self.local_user)
self.assertEqual(models.Status.objects.count(), 2) self.assertEqual(models.Status.objects.count(), 2)
@ -284,7 +284,7 @@ class Incoming(TestCase):
incoming.handle_create(activity) incoming.handle_create(activity)
status = models.Status.objects.last() status = models.Status.objects.last()
self.assertEqual(status.content, 'test content in note') self.assertEqual(status.content, '<p>test content in note</p>')
self.assertEqual(status.mention_users.first(), self.local_user) self.assertEqual(status.mention_users.first(), self.local_user)
self.assertTrue( self.assertTrue(
models.Notification.objects.filter(user=self.local_user).exists()) models.Notification.objects.filter(user=self.local_user).exists())
@ -306,7 +306,7 @@ class Incoming(TestCase):
incoming.handle_create(activity) incoming.handle_create(activity)
status = models.Status.objects.last() status = models.Status.objects.last()
self.assertEqual(status.content, 'test content in note') self.assertEqual(status.content, '<p>test content in note</p>')
self.assertEqual(status.reply_parent, self.status) self.assertEqual(status.reply_parent, self.status)
self.assertTrue( self.assertTrue(
models.Notification.objects.filter(user=self.local_user)) models.Notification.objects.filter(user=self.local_user))

View file

@ -615,18 +615,13 @@ def book_page(request, book_id):
book__parent_work=book.parent_work, book__parent_work=book.parent_work,
) )
rating = reviews.aggregate(Avg('rating'))
tags = models.UserTag.objects.filter(
book=book,
)
data = { data = {
'title': book.title, 'title': book.title,
'book': book, 'book': book,
'reviews': reviews_page, 'reviews': reviews_page,
'ratings': reviews.filter(content__isnull=True), 'ratings': reviews.filter(content__isnull=True),
'rating': rating['rating__avg'], 'rating': reviews.aggregate(Avg('rating'))['rating__avg'],
'tags': tags, 'tags': models.UserTag.objects.filter(book=book),
'user_tags': user_tags, 'user_tags': user_tags,
'user_shelves': user_shelves, 'user_shelves': user_shelves,
'other_edition_shelves': other_edition_shelves, 'other_edition_shelves': other_edition_shelves,
@ -761,7 +756,7 @@ def shelf_page(request, username, shelf_identifier):
return JsonResponse(shelf.to_activity(**request.GET)) return JsonResponse(shelf.to_activity(**request.GET))
data = { data = {
'title': user.name, 'title': '%s\'s %s shelf' % (user.display_name, shelf.name),
'user': user, 'user': user,
'is_self': is_self, 'is_self': is_self,
'shelves': shelves.all(), 'shelves': shelves.all(),

View file

@ -4,6 +4,7 @@ Django==3.0.7
django-model-utils==4.0.0 django-model-utils==4.0.0
environs==7.2.0 environs==7.2.0
flower==0.9.4 flower==0.9.4
Markdown==3.3.3
Pillow>=7.1.0 Pillow>=7.1.0
psycopg2==2.8.4 psycopg2==2.8.4
pycryptodome==3.9.4 pycryptodome==3.9.4