mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-23 08:36:32 +00:00
Merge pull request #409 from mouse-reeve/html-fields
Allow markdown in html fields
This commit is contained in:
commit
a771b1d5b6
9 changed files with 48 additions and 28 deletions
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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> — {% include 'snippets/book_titleby.html' with book=status.book %}</p>
|
<p> — {% include 'snippets/book_titleby.html' with book=status.book %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue