From aacf5b7ba41d7ac5196aaecf1d8cab3cd5f9c398 Mon Sep 17 00:00:00 2001
From: Mouse Reeve <mousereeve@riseup.net>
Date: Sat, 12 Dec 2020 18:00:39 -0800
Subject: [PATCH 1/8] fields for content warnings

---
 bookwyrm/activitypub/note.py | 1 +
 bookwyrm/models/status.py    | 4 +++-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py
index df28bf8de..263ddb2a6 100644
--- a/bookwyrm/activitypub/note.py
+++ b/bookwyrm/activitypub/note.py
@@ -23,6 +23,7 @@ class Note(ActivityObject):
     cc: List[str] = field(default_factory=lambda: [])
     replies: Dict = field(default_factory=lambda: {})
     inReplyTo: str = ''
+    summary: str = ''
     tag: List[Link] = field(default_factory=lambda: [])
     attachment: List[Image] = field(default_factory=lambda: [])
     sensitive: bool = False
diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py
index 55036f2c9..308a3c8c0 100644
--- a/bookwyrm/models/status.py
+++ b/bookwyrm/models/status.py
@@ -23,6 +23,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
         default='public',
         choices=PrivacyLevels.choices
     )
+    content_warning = fields.CharField(
+        max_length=150, blank=True, null=True, activitypub_field='summary')
     sensitive = fields.BooleanField(default=False)
     # the created date can't be this, because of receiving federated posts
     published_date = fields.DateTimeField(
@@ -68,7 +70,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
             **kwargs
         )
 
-    def to_activity(self, pure=False):
+    def to_activity(self, pure=False):# pylint: disable=arguments-differ
         ''' return tombstone if the status is deleted '''
         if self.deleted:
             return activitypub.Tombstone(

From f1926ce76d79d5a9917f7e75ca7c4352e9af4176 Mon Sep 17 00:00:00 2001
From: Mouse Reeve <mousereeve@riseup.net>
Date: Wed, 16 Dec 2020 16:20:40 -0800
Subject: [PATCH 2/8] Avoid duplicate notifitions

And render html
---
 bookwyrm/incoming.py                  | 5 ++++-
 bookwyrm/templates/notifications.html | 2 +-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 556c34a27..78e7b9707 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -218,7 +218,9 @@ def handle_create(activity):
 
     status = activity.to_model(model)
     # create a notification if this is a reply
+    notified = []
     if status.reply_parent and status.reply_parent.user.local:
+        notified.append(status.reply_parent.user)
         status_builder.create_notification(
             status.reply_parent.user,
             'REPLY',
@@ -226,7 +228,8 @@ def handle_create(activity):
             related_status=status,
         )
     if status.mention_users.exists():
-        for mentioned_user in status.mention_users.all():
+        for mentioned_user in status.mention_users.all() and \
+                mentioned_user not in notified:
             if not mentioned_user.local:
                 continue
             status_builder.create_notification(
diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html
index f31df76d8..ddcbc0fda 100644
--- a/bookwyrm/templates/notifications.html
+++ b/bookwyrm/templates/notifications.html
@@ -54,7 +54,7 @@
             <div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white{% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %} has-text-black{% else %}-bis has-text-grey-dark{% endif %}{% endif %}">
                 <div class="columns">
                     <div class="column">
-                        <a href="{{ notification.related_status.remote_id }}">{{ notification.related_status.content | truncatewords_html:10 }}</a>
+                        <a href="{{ notification.related_status.remote_id }}">{{ notification.related_status.content | safe | truncatewords_html:10 }}</a>
                     </div>
                     <div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
                         {{ notification.related_status.published_date | post_date }}

From a3c7d324d67fe29202e76fb332b7c328e61faf3c Mon Sep 17 00:00:00 2001
From: Mouse Reeve <mousereeve@riseup.net>
Date: Wed, 16 Dec 2020 16:47:05 -0800
Subject: [PATCH 3/8] Sanitize incoming html

---
 .../migrations/0025_auto_20201217_0046.py     | 39 +++++++++++++++++++
 bookwyrm/models/author.py                     |  2 +-
 bookwyrm/models/book.py                       |  2 +-
 bookwyrm/models/fields.py                     | 10 +++++
 bookwyrm/models/status.py                     |  4 +-
 bookwyrm/models/user.py                       |  2 +-
 bookwyrm/sanitize_html.py                     |  2 +-
 bookwyrm/tests/test_sanitize_html.py          | 12 +++---
 8 files changed, 62 insertions(+), 11 deletions(-)
 create mode 100644 bookwyrm/migrations/0025_auto_20201217_0046.py

diff --git a/bookwyrm/migrations/0025_auto_20201217_0046.py b/bookwyrm/migrations/0025_auto_20201217_0046.py
new file mode 100644
index 000000000..a3ffe8c13
--- /dev/null
+++ b/bookwyrm/migrations/0025_auto_20201217_0046.py
@@ -0,0 +1,39 @@
+# Generated by Django 3.0.7 on 2020-12-17 00:46
+
+import bookwyrm.models.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('bookwyrm', '0024_merge_20201216_1721'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='author',
+            name='bio',
+            field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='book',
+            name='description',
+            field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='quotation',
+            name='quote',
+            field=bookwyrm.models.fields.HtmlField(),
+        ),
+        migrations.AlterField(
+            model_name='status',
+            name='content',
+            field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='user',
+            name='summary',
+            field=bookwyrm.models.fields.HtmlField(default=''),
+        ),
+    ]
diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py
index 331d2dd6f..47714d4ec 100644
--- a/bookwyrm/models/author.py
+++ b/bookwyrm/models/author.py
@@ -25,7 +25,7 @@ class Author(ActivitypubMixin, BookWyrmModel):
     aliases = fields.ArrayField(
         models.CharField(max_length=255), blank=True, default=list
     )
-    bio = fields.TextField(null=True, blank=True)
+    bio = fields.HtmlField(null=True, blank=True)
 
     def save(self, *args, **kwargs):
         ''' can't be abstract for query reasons, but you shouldn't USE it '''
diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py
index bcd4bc046..3080a1158 100644
--- a/bookwyrm/models/book.py
+++ b/bookwyrm/models/book.py
@@ -36,7 +36,7 @@ class Book(ActivitypubMixin, BookWyrmModel):
     title = fields.CharField(max_length=255)
     sort_title = fields.CharField(max_length=255, blank=True, null=True)
     subtitle = fields.CharField(max_length=255, blank=True, null=True)
-    description = fields.TextField(blank=True, null=True)
+    description = fields.HtmlField(blank=True, null=True)
     languages = fields.ArrayField(
         models.CharField(max_length=255), blank=True, default=list
     )
diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py
index 5e12f5d56..529337150 100644
--- a/bookwyrm/models/fields.py
+++ b/bookwyrm/models/fields.py
@@ -12,6 +12,7 @@ from django.db import models
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 from bookwyrm import activitypub
+from bookwyrm.sanitize_html import InputHtmlParser
 from bookwyrm.settings import DOMAIN
 from bookwyrm.connectors import get_image
 
@@ -362,6 +363,15 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
         except (ParserError, TypeError):
             return None
 
+class HtmlField(ActivitypubFieldMixin, models.TextField):
+    ''' a text field for storing html '''
+    def field_from_activity(self, value):
+        if not value or value == MISSING:
+            return None
+        sanitizer = InputHtmlParser()
+        sanitizer.feed(value)
+        return sanitizer.get_output()
+
 class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
     ''' activitypub-aware array field '''
     def field_to_activity(self, value):
diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py
index fcf4a2907..66114e7cc 100644
--- a/bookwyrm/models/status.py
+++ b/bookwyrm/models/status.py
@@ -14,7 +14,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
     ''' any post, like a reply to a review, etc '''
     user = fields.ForeignKey(
         'User', on_delete=models.PROTECT, activitypub_field='attributedTo')
-    content = fields.TextField(blank=True, null=True)
+    content = fields.HtmlField(blank=True, null=True)
     mention_users = fields.TagField('User', related_name='mention_user')
     mention_books = fields.TagField('Edition', related_name='mention_book')
     local = models.BooleanField(default=True)
@@ -134,7 +134,7 @@ class Comment(Status):
 
 class Quotation(Status):
     ''' like a review but without a rating and transient '''
-    quote = fields.TextField()
+    quote = fields.HtmlField()
     book = fields.ForeignKey(
         'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
 
diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py
index 63549d360..9d66eb5cf 100644
--- a/bookwyrm/models/user.py
+++ b/bookwyrm/models/user.py
@@ -42,7 +42,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
         blank=True,
     )
     outbox = fields.RemoteIdField(unique=True)
-    summary = fields.TextField(default='')
+    summary = fields.HtmlField(default='')
     local = models.BooleanField(default=False)
     bookwyrm_user = fields.BooleanField(default=True)
     localname = models.CharField(
diff --git a/bookwyrm/sanitize_html.py b/bookwyrm/sanitize_html.py
index 9c5ca73ac..933fc43c6 100644
--- a/bookwyrm/sanitize_html.py
+++ b/bookwyrm/sanitize_html.py
@@ -1,7 +1,7 @@
 ''' html parser to clean up incoming text from unknown sources '''
 from html.parser import HTMLParser
 
-class InputHtmlParser(HTMLParser):
+class InputHtmlParser(HTMLParser):#pylint: disable=abstract-method
     ''' Removes any html that isn't allowed_tagsed from a block '''
 
     def __init__(self):
diff --git a/bookwyrm/tests/test_sanitize_html.py b/bookwyrm/tests/test_sanitize_html.py
index 3344a9347..58d94311c 100644
--- a/bookwyrm/tests/test_sanitize_html.py
+++ b/bookwyrm/tests/test_sanitize_html.py
@@ -1,34 +1,36 @@
+''' make sure only valid html gets to the app '''
 from django.test import TestCase
 
 from bookwyrm.sanitize_html import InputHtmlParser
 
-
 class Sanitizer(TestCase):
+    ''' sanitizer tests '''
     def test_no_html(self):
+        ''' just text '''
         input_text = 'no      html  '
         parser = InputHtmlParser()
         parser.feed(input_text)
         output = parser.get_output()
         self.assertEqual(input_text, output)
 
-
     def test_valid_html(self):
+        ''' leave the html untouched '''
         input_text = '<b>yes    </b> <i>html</i>'
         parser = InputHtmlParser()
         parser.feed(input_text)
         output = parser.get_output()
         self.assertEqual(input_text, output)
 
-
     def test_valid_html_attrs(self):
+        ''' and don't remove attributes '''
         input_text = '<a href="fish.com">yes    </a> <i>html</i>'
         parser = InputHtmlParser()
         parser.feed(input_text)
         output = parser.get_output()
         self.assertEqual(input_text, output)
 
-
     def test_invalid_html(self):
+        ''' remove all html when the html is malformed '''
         input_text = '<b>yes  <i>html</i>'
         parser = InputHtmlParser()
         parser.feed(input_text)
@@ -41,8 +43,8 @@ class Sanitizer(TestCase):
         output = parser.get_output()
         self.assertEqual('yes html   ', output)
 
-
     def test_disallowed_html(self):
+        ''' remove disallowed html but keep allowed html '''
         input_text = '<div>  yes <i>html</i></div>'
         parser = InputHtmlParser()
         parser.feed(input_text)

From 42167af3e9dad8aa040b74f66f4b50c0ea98a77b Mon Sep 17 00:00:00 2001
From: Mouse Reeve <mousereeve@riseup.net>
Date: Wed, 16 Dec 2020 18:39:18 -0800
Subject: [PATCH 4/8] Tests fro html field

---
 bookwyrm/tests/models/test_fields.py | 63 +++++++++++++++++++++-------
 1 file changed, 48 insertions(+), 15 deletions(-)

diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py
index 8c86b23ce..81b1d528a 100644
--- a/bookwyrm/tests/models/test_fields.py
+++ b/bookwyrm/tests/models/test_fields.py
@@ -21,31 +21,23 @@ from bookwyrm.activitypub.base_activity import ActivityObject
 from bookwyrm.models import fields, User, Status
 from bookwyrm.models.base_model import ActivitypubMixin, BookWyrmModel
 
+#pylint: disable=too-many-public-methods
 class ActivitypubFields(TestCase):
     ''' overwrites standard model feilds to work with activitypub '''
     def test_validate_remote_id(self):
         ''' should look like a url '''
-        self.assertIsNone(fields.validate_remote_id(
-            'http://www.example.com'
-        ))
-        self.assertIsNone(fields.validate_remote_id(
-            'https://www.example.com'
-        ))
-        self.assertIsNone(fields.validate_remote_id(
-            'http://example.com/dlfjg-23/x'
-        ))
+        self.assertIsNone(fields.validate_remote_id('http://www.example.com'))
+        self.assertIsNone(fields.validate_remote_id('https://www.example.com'))
+        self.assertIsNone(fields.validate_remote_id('http://exle.com/dlg-23/x'))
         self.assertRaises(
             ValidationError, fields.validate_remote_id,
-            'http:/example.com/dlfjg-23/x'
-        )
+            'http:/example.com/dlfjg-23/x')
         self.assertRaises(
             ValidationError, fields.validate_remote_id,
-            'www.example.com/dlfjg-23/x'
-        )
+            'www.example.com/dlfjg-23/x')
         self.assertRaises(
             ValidationError, fields.validate_remote_id,
-            'http://www.example.com/dlfjg 23/x'
-        )
+            'http://www.example.com/dlfjg 23/x')
 
     def test_activitypub_field_mixin(self):
         ''' generic mixin with super basic to and from functionality '''
@@ -71,6 +63,38 @@ class ActivitypubFields(TestCase):
         instance.name = 'snake_case_name'
         self.assertEqual(instance.get_activitypub_field(), 'snakeCaseName')
 
+    def test_set_field_from_activity(self):
+        ''' setter from entire json blob '''
+        @dataclass
+        class TestModel:
+            ''' real simple mock '''
+            field_name: str
+
+        mock_model = TestModel(field_name='bip')
+        TestActivity = namedtuple('test', ('fieldName', 'unrelated'))
+        data = TestActivity(fieldName='hi', unrelated='bfkjh')
+
+        instance = fields.ActivitypubFieldMixin()
+        instance.name = 'field_name'
+
+        instance.set_field_from_activity(mock_model, data)
+        self.assertEqual(mock_model.field_name, 'hi')
+
+    def test_set_activity_from_field(self):
+        ''' set json field given entire model '''
+        @dataclass
+        class TestModel:
+            ''' real simple mock '''
+            field_name: str
+            unrelated: str
+        mock_model = TestModel(field_name='bip', unrelated='field')
+        instance = fields.ActivitypubFieldMixin()
+        instance.name = 'field_name'
+
+        data = {}
+        instance.set_activity_from_field(data, mock_model)
+        self.assertEqual(data['fieldName'], 'bip')
+
     def test_remote_id_field(self):
         ''' just sets some defaults on charfield '''
         instance = fields.RemoteIdField()
@@ -408,3 +432,12 @@ class ActivitypubFields(TestCase):
         ''' idk why it makes them strings but probably for a good reason '''
         instance = fields.ArrayField(fields.IntegerField)
         self.assertEqual(instance.field_to_activity([0, 1]), ['0', '1'])
+
+
+    def test_html_field(self):
+        ''' sanitizes html, the sanitizer has its own tests '''
+        instance = fields.HtmlField()
+        self.assertEqual(
+            instance.field_from_activity('<marquee><p>hi</p></marquee>'),
+            '<p>hi</p>'
+        )

From f7cb52598144aa896e8d9518dbb67b03b1b8b70c Mon Sep 17 00:00:00 2001
From: Mouse Reeve <mousereeve@riseup.net>
Date: Wed, 16 Dec 2020 18:40:43 -0800
Subject: [PATCH 5/8] Fixes logic error

---
 bookwyrm/incoming.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py
index 78e7b9707..c1c15ca93 100644
--- a/bookwyrm/incoming.py
+++ b/bookwyrm/incoming.py
@@ -228,9 +228,8 @@ def handle_create(activity):
             related_status=status,
         )
     if status.mention_users.exists():
-        for mentioned_user in status.mention_users.all() and \
-                mentioned_user not in notified:
-            if not mentioned_user.local:
+        for mentioned_user in status.mention_users.all():
+            if not mentioned_user.local or mentioned_user in notified:
                 continue
             status_builder.create_notification(
                 mentioned_user,

From b796686483e26011a22954673d143128b150e3b9 Mon Sep 17 00:00:00 2001
From: Mouse Reeve <mousereeve@riseup.net>
Date: Wed, 16 Dec 2020 19:20:15 -0800
Subject: [PATCH 6/8] Adds cw field

---
 .../migrations/0026_status_content_warning.py | 19 +++++++++++++++++++
 bookwyrm/models/status.py                     |  2 +-
 2 files changed, 20 insertions(+), 1 deletion(-)
 create mode 100644 bookwyrm/migrations/0026_status_content_warning.py

diff --git a/bookwyrm/migrations/0026_status_content_warning.py b/bookwyrm/migrations/0026_status_content_warning.py
new file mode 100644
index 000000000..f4e494db9
--- /dev/null
+++ b/bookwyrm/migrations/0026_status_content_warning.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.7 on 2020-12-17 03:17
+
+import bookwyrm.models.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('bookwyrm', '0025_auto_20201217_0046'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='status',
+            name='content_warning',
+            field=bookwyrm.models.fields.CharField(blank=True, max_length=500, null=True),
+        ),
+    ]
diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py
index 18b9431e4..49fbad55f 100644
--- a/bookwyrm/models/status.py
+++ b/bookwyrm/models/status.py
@@ -19,7 +19,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
     mention_books = fields.TagField('Edition', related_name='mention_book')
     local = models.BooleanField(default=True)
     content_warning = fields.CharField(
-        max_length=150, blank=True, null=True, activitypub_field='summary')
+        max_length=500, blank=True, null=True, activitypub_field='summary')
     privacy = fields.PrivacyField(max_length=255)
     sensitive = fields.BooleanField(default=False)
     # created date is different than publish date because of federated posts

From 0d42b9cf8f765e2b6338b064f4d213a4130d2793 Mon Sep 17 00:00:00 2001
From: Mouse Reeve <mousereeve@riseup.net>
Date: Wed, 16 Dec 2020 19:50:36 -0800
Subject: [PATCH 7/8] Display status cw's

---
 .../templates/snippets/status_content.html    | 77 +++++++++++--------
 1 file changed, 47 insertions(+), 30 deletions(-)

diff --git a/bookwyrm/templates/snippets/status_content.html b/bookwyrm/templates/snippets/status_content.html
index 2ef06421a..ff87b3b6a 100644
--- a/bookwyrm/templates/snippets/status_content.html
+++ b/bookwyrm/templates/snippets/status_content.html
@@ -1,38 +1,55 @@
 {% load bookwyrm_tags %}
 <div class="block">
-    {% if status.status_type == 'Review' %}
-    <h3>
-        {% if status.name %}{{ status.name }}<br>{% endif %}
-        {% include 'snippets/stars.html' with rating=status.rating %}
-    </h3>
-    {% endif %}
-
-    {% if status.quote %}
-    <div class="quote block">
-        <blockquote>{{ status.quote }}</blockquote>
-
-        <p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p>
-    </div>
-    {% endif %}
-
-    {% if status.content and status.status_type != 'GeneratedNote' and status.status_type != 'Boost' %}
-    {% include 'snippets/trimmed_text.html' with full=status.content|safe %}
-    {% endif %}
-    {% if status.attachments %}
-    <div class="block">
-        <div class="columns">
-            {% for attachment in status.attachments.all %}
-            <div class="column is-narrow">
-                <figure class="image is-128x128">
-                    <a href="/images/{{ attachment.image }}" target="_blank" aria-label="open image in new window">
-                        <img src="/images/{{ attachment.image }}" alt="{{ attachment.caption }}">
-                    </a>
-                </figure>
-            </div>
-            {% endfor %}
+    {% if status.content_warning %}
+    <div class="toggle-content">
+        <p>{{ status.content_warning }}</p>
+        <input class="toggle-control" type="radio" name="toggle-status-cw-{{ status.id }}" id="hide-status-cw-{{ status.id }}" checked>
+        <div class="toggle-content hidden">
+            <label class="button is-small" for="show-status-cw-{{ status.id }}" tabindex="0" role="button">Show More</label>
         </div>
     </div>
+
+    <input class="toggle-control" type="radio" name="toggle-status-cw-{{ status.id }}" id="show-status-cw-{{ status.id }}">
     {% endif %}
+    <div{% if status.content_warning %} class="toggle-content hidden"{% endif %}>
+        {% if status.content_warning %}
+        <label class="button is-small" for="hide-status-cw-{{ status.id }}" tabindex="0" role="button">Show Less</label>
+        {% endif %}
+
+        {% if status.status_type == 'Review' %}
+        <h3>
+            {% if status.name %}{{ status.name }}<br>{% endif %}
+            {% include 'snippets/stars.html' with rating=status.rating %}
+        </h3>
+        {% endif %}
+
+        {% if status.quote %}
+        <div class="quote block">
+            <blockquote>{{ status.quote }}</blockquote>
+
+            <p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p>
+        </div>
+        {% endif %}
+
+        {% if status.content and status.status_type != 'GeneratedNote' and status.status_type != 'Boost' %}
+        {% include 'snippets/trimmed_text.html' with full=status.content|safe %}
+        {% endif %}
+        {% if status.attachments %}
+        <div class="block">
+            <div class="columns">
+                {% for attachment in status.attachments.all %}
+                <div class="column is-narrow">
+                    <figure class="image is-128x128">
+                        <a href="/images/{{ attachment.image }}" target="_blank" aria-label="open image in new window">
+                            <img src="/images/{{ attachment.image }}" alt="{{ attachment.caption }}">
+                        </a>
+                    </figure>
+                </div>
+                {% endfor %}
+            </div>
+        </div>
+        {% endif %}
+    </div>
 </div>
 
 {% if not hide_book %}

From 172c36b6416c05997e18ce0b75a6ed143bf34ae2 Mon Sep 17 00:00:00 2001
From: Mouse Reeve <mousereeve@riseup.net>
Date: Wed, 16 Dec 2020 20:10:50 -0800
Subject: [PATCH 8/8] Adds content warning field in status forms

---
 bookwyrm/activitypub/note.py                     |  2 +-
 bookwyrm/forms.py                                | 12 ++++++++----
 .../templates/snippets/create_status_form.html   | 10 ++++++++++
 bookwyrm/templates/snippets/reply_form.html      |  8 ++++++++
 bookwyrm/templates/snippets/status_content.html  | 16 +++++++++-------
 5 files changed, 36 insertions(+), 12 deletions(-)

diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py
index 263ddb2a6..b478c96dd 100644
--- a/bookwyrm/activitypub/note.py
+++ b/bookwyrm/activitypub/note.py
@@ -54,7 +54,7 @@ class Comment(Note):
 class Review(Comment):
     ''' a full book review '''
     name: str
-    rating: int
+    rating: int = None
     type: str = 'Review'
 
 
diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py
index a2c3e24bb..454836bb7 100644
--- a/bookwyrm/forms.py
+++ b/bookwyrm/forms.py
@@ -60,25 +60,29 @@ class RatingForm(CustomForm):
 class ReviewForm(CustomForm):
     class Meta:
         model = models.Review
-        fields = ['user', 'book', 'name', 'content', 'rating', 'privacy']
+        fields = [
+            'user', 'book', 'name', 'content', 'content_warning', 'rating',
+            'privacy']
 
 
 class CommentForm(CustomForm):
     class Meta:
         model = models.Comment
-        fields = ['user', 'book', 'content', 'privacy']
+        fields = ['user', 'book', 'content', 'content_warning', 'privacy']
 
 
 class QuotationForm(CustomForm):
     class Meta:
         model = models.Quotation
-        fields = ['user', 'book', 'quote', 'content', 'privacy']
+        fields = [
+            'user', 'book', 'quote', 'content', 'content_warning', 'privacy']
 
 
 class ReplyForm(CustomForm):
     class Meta:
         model = models.Status
-        fields = ['user', 'content', 'reply_parent', 'privacy']
+        fields = [
+            'user', 'content', 'content_warning', 'reply_parent', 'privacy']
 
 
 class EditUserForm(CustomForm):
diff --git a/bookwyrm/templates/snippets/create_status_form.html b/bookwyrm/templates/snippets/create_status_form.html
index d6aa3fb3c..70062db4d 100644
--- a/bookwyrm/templates/snippets/create_status_form.html
+++ b/bookwyrm/templates/snippets/create_status_form.html
@@ -26,6 +26,16 @@
             </div>
         </fieldset>
         {% endif %}
+
+        <div class="control">
+            <label class="button is-small" role="button" tabindex="0" for="include-spoilers-{{ book.id }}-{{ type }}">Add spoilers/content warning</label>
+            <input type="checkbox" class="toggle-control" id="include-spoilers-{{ book.id }}-{{ type }}">
+            <div class="toggle-content hidden">
+                <label class="is-sr-only" for="id_content_warning_{{ book.id }}_{{ type }}">Spoilers/content warning:</label>
+                <input type="text" name="content_warning" maxlength="255" class="input" id="id_content_warning_{{ book.id }}_{{ type }}" placeholder="Spoilers ahead!">
+            </div>
+        </div>
+
         {% if type == 'quote' %}
         <textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
         {% else %}
diff --git a/bookwyrm/templates/snippets/reply_form.html b/bookwyrm/templates/snippets/reply_form.html
index d0a0f6b9d..65aa3e46a 100644
--- a/bookwyrm/templates/snippets/reply_form.html
+++ b/bookwyrm/templates/snippets/reply_form.html
@@ -6,6 +6,14 @@
         <input type="hidden" name="reply_parent" value="{{ activity.id }}">
         <input type="hidden" name="user" value="{{ request.user.id }}">
         <div class="column">
+            <div class="control">
+                <label class="button is-small" role="button" tabindex="0" for="include-spoilers-{{ book.id }}-{{ type }}">Add spoilers/content warning</label>
+                <input type="checkbox" class="toggle-control" id="include-spoilers-{{ book.id }}-{{ type }}">
+                <div class="toggle-content hidden">
+                    <label class="is-sr-only" for="id_content_warning_{{ book.id }}_{{ type }}">Spoilers/content warning:</label>
+                    <input type="text" name="content_warning" maxlength="255" class="input" id="id_content_warning_{{ book.id }}_{{ type }}" placeholder="Spoilers ahead!">
+                </div>
+            </div>
             <div class="field">
                 <textarea class="textarea" name="content" placeholder="Leave a comment..." id="id_content_{{ activity.id }}-{{ uuid }}" required="true"></textarea>
             </div>
diff --git a/bookwyrm/templates/snippets/status_content.html b/bookwyrm/templates/snippets/status_content.html
index ff87b3b6a..6e4a6b983 100644
--- a/bookwyrm/templates/snippets/status_content.html
+++ b/bookwyrm/templates/snippets/status_content.html
@@ -1,5 +1,14 @@
 {% load bookwyrm_tags %}
 <div class="block">
+    {% if status.status_type == 'Review' %}
+    <div>
+        <h3 class="title is-5 has-subtitle">
+            {% if status.name %}{{ status.name }}<br>{% endif %}
+        </h3>
+        <p class="subtitle">{% include 'snippets/stars.html' with rating=status.rating %}</p>
+    </div>
+    {% endif %}
+
     {% if status.content_warning %}
     <div class="toggle-content">
         <p>{{ status.content_warning }}</p>
@@ -16,13 +25,6 @@
         <label class="button is-small" for="hide-status-cw-{{ status.id }}" tabindex="0" role="button">Show Less</label>
         {% endif %}
 
-        {% if status.status_type == 'Review' %}
-        <h3>
-            {% if status.name %}{{ status.name }}<br>{% endif %}
-            {% include 'snippets/stars.html' with rating=status.rating %}
-        </h3>
-        {% endif %}
-
         {% if status.quote %}
         <div class="quote block">
             <blockquote>{{ status.quote }}</blockquote>