We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Clarify UI around content warnings
A few problems with content warnings:
 - [ ] The button is icon-only and hard to find for visual users in the compose UI (I think it's fine for screen reader users):
<img width="360" alt="Screen Shot 2021-09-18 at 11 11 48 AM" src="https://user-images.githubusercontent.com/1807695/133898456-ec77f0d2-febd-428f-aa70-4702e8576d31.png">

 - [x] ~When content warnings are active in the compose box, they aren't clearly labeled (again, for visual users only):~ (fixed by #1422)
<img width="357" alt="Screen Shot 2021-09-18 at 11 12 44 AM" src="https://user-images.githubusercontent.com/1807695/133898461-71879dc4-7a69-4280-a33f-7607746b8454.png">

- [x] #1430
<img width="779" alt="Screen Shot 2021-09-18 at 11 13 09 AM" src="https://user-images.githubusercontent.com/1807695/133898481-c63000d9-1ffd-4742-89ec-8e0558921c30.png">


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bookwyrm/views/status.py`
Content:
```
1 """ what are we here for if not for posting """
2 import re
3 from urllib.parse import urlparse
4 
5 from django.contrib.auth.decorators import login_required
6 from django.core.validators import URLValidator
7 from django.core.exceptions import ValidationError
8 from django.http import HttpResponse, HttpResponseBadRequest, Http404
9 from django.shortcuts import get_object_or_404, redirect
10 from django.template.response import TemplateResponse
11 from django.utils import timezone
12 from django.utils.decorators import method_decorator
13 from django.views import View
14 from django.views.decorators.http import require_POST
15 
16 from markdown import markdown
17 from bookwyrm import forms, models
18 from bookwyrm.sanitize_html import InputHtmlParser
19 from bookwyrm.settings import DOMAIN
20 from bookwyrm.utils import regex
21 from .helpers import handle_remote_webfinger, is_api_request
22 from .helpers import load_date_in_user_tz_as_utc
23 
24 
25 # pylint: disable= no-self-use
26 @method_decorator(login_required, name="dispatch")
27 class EditStatus(View):
28     """the view for *posting*"""
29 
30     def get(self, request, status_id):  # pylint: disable=unused-argument
31         """load the edit panel"""
32         status = get_object_or_404(
33             models.Status.objects.select_subclasses(), id=status_id
34         )
35         status.raise_not_editable(request.user)
36 
37         status_type = "reply" if status.reply_parent else status.status_type.lower()
38         data = {
39             "type": status_type,
40             "book": getattr(status, "book", None),
41             "draft": status,
42         }
43         return TemplateResponse(request, "compose.html", data)
44 
45 
46 # pylint: disable= no-self-use
47 @method_decorator(login_required, name="dispatch")
48 class CreateStatus(View):
49     """the view for *posting*"""
50 
51     def get(self, request, status_type):  # pylint: disable=unused-argument
52         """compose view (...not used?)"""
53         book = get_object_or_404(models.Edition, id=request.GET.get("book"))
54         data = {"book": book}
55         return TemplateResponse(request, "compose.html", data)
56 
57     # pylint: disable=too-many-branches
58     def post(self, request, status_type, existing_status_id=None):
59         """create status of whatever type"""
60         created = not existing_status_id
61         existing_status = None
62         if existing_status_id:
63             existing_status = get_object_or_404(
64                 models.Status.objects.select_subclasses(), id=existing_status_id
65             )
66             existing_status.raise_not_editable(request.user)
67             existing_status.edited_date = timezone.now()
68 
69         status_type = status_type[0].upper() + status_type[1:]
70 
71         try:
72             form = getattr(forms, f"{status_type}Form")(
73                 request.POST, instance=existing_status
74             )
75         except AttributeError:
76             return HttpResponseBadRequest()
77         if not form.is_valid():
78             if is_api_request(request):
79                 return HttpResponse(status=500)
80             return redirect(request.headers.get("Referer", "/"))
81 
82         status = form.save(commit=False)
83         # save the plain, unformatted version of the status for future editing
84         status.raw_content = status.content
85         if hasattr(status, "quote"):
86             status.raw_quote = status.quote
87 
88         if not status.sensitive and status.content_warning:
89             # the cw text field remains populated when you click "remove"
90             status.content_warning = None
91         status.save(broadcast=False)
92 
93         # inspect the text for user tags
94         content = status.content
95         for (mention_text, mention_user) in find_mentions(content):
96             # add them to status mentions fk
97             status.mention_users.add(mention_user)
98 
99             # turn the mention into a link
100             content = re.sub(
101                 rf"{mention_text}([^@]|$)",
102                 rf'<a href="{mention_user.remote_id}">{mention_text}</a>\g<1>',
103                 content,
104             )
105         # add reply parent to mentions
106         if status.reply_parent:
107             status.mention_users.add(status.reply_parent.user)
108 
109         # deduplicate mentions
110         status.mention_users.set(set(status.mention_users.all()))
111 
112         # don't apply formatting to generated notes
113         if not isinstance(status, models.GeneratedNote) and content:
114             status.content = to_markdown(content)
115         # do apply formatting to quotes
116         if hasattr(status, "quote"):
117             status.quote = to_markdown(status.quote)
118 
119         status.save(created=created)
120 
121         # update a readthrough, if needed
122         if bool(request.POST.get("id")):
123             try:
124                 edit_readthrough(request)
125             except Http404:
126                 pass
127 
128         if is_api_request(request):
129             return HttpResponse()
130         return redirect("/")
131 
132 
133 @method_decorator(login_required, name="dispatch")
134 class DeleteStatus(View):
135     """tombstone that bad boy"""
136 
137     def post(self, request, status_id):
138         """delete and tombstone a status"""
139         status = get_object_or_404(models.Status, id=status_id)
140 
141         # don't let people delete other people's statuses
142         status.raise_not_deletable(request.user)
143 
144         # perform deletion
145         status.delete()
146         return redirect(request.headers.get("Referer", "/"))
147 
148 
149 @login_required
150 @require_POST
151 def update_progress(request, book_id):  # pylint: disable=unused-argument
152     """Either it's just a progress update, or it's a comment with a progress update"""
153     if request.POST.get("post-status"):
154         return CreateStatus.as_view()(request, "comment")
155     return edit_readthrough(request)
156 
157 
158 @login_required
159 @require_POST
160 def edit_readthrough(request):
161     """can't use the form because the dates are too finnicky"""
162     # TODO: remove this, it duplicates the code in the ReadThrough view
163     readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
164     readthrough.raise_not_editable(request.user)
165 
166     readthrough.start_date = load_date_in_user_tz_as_utc(
167         request.POST.get("start_date"), request.user
168     )
169     readthrough.finish_date = load_date_in_user_tz_as_utc(
170         request.POST.get("finish_date"), request.user
171     )
172 
173     progress = request.POST.get("progress")
174     try:
175         progress = int(progress)
176         readthrough.progress = progress
177     except (ValueError, TypeError):
178         pass
179 
180     progress_mode = request.POST.get("progress_mode")
181     try:
182         progress_mode = models.ProgressMode(progress_mode)
183         readthrough.progress_mode = progress_mode
184     except ValueError:
185         pass
186 
187     readthrough.save()
188 
189     # record the progress update individually
190     # use default now for date field
191     readthrough.create_update()
192 
193     if is_api_request(request):
194         return HttpResponse()
195     return redirect(request.headers.get("Referer", "/"))
196 
197 
198 def find_mentions(content):
199     """detect @mentions in raw status content"""
200     if not content:
201         return
202     for match in re.finditer(regex.STRICT_USERNAME, content):
203         username = match.group().strip().split("@")[1:]
204         if len(username) == 1:
205             # this looks like a local user (@user), fill in the domain
206             username.append(DOMAIN)
207         username = "@".join(username)
208 
209         mention_user = handle_remote_webfinger(username)
210         if not mention_user:
211             # we can ignore users we don't know about
212             continue
213         yield (match.group(), mention_user)
214 
215 
216 def format_links(content):
217     """detect and format links"""
218     validator = URLValidator()
219     formatted_content = ""
220     split_content = re.split(r"(\s+)", content)
221 
222     for potential_link in split_content:
223         if not potential_link:
224             continue
225         wrapped = _wrapped(potential_link)
226         if wrapped:
227             wrapper_close = potential_link[-1]
228             formatted_content += potential_link[0]
229             potential_link = potential_link[1:-1]
230 
231         try:
232             # raises an error on anything that's not a valid link
233             validator(potential_link)
234 
235             # use everything but the scheme in the presentation of the link
236             url = urlparse(potential_link)
237             link = url.netloc + url.path + url.params
238             if url.query != "":
239                 link += "?" + url.query
240             if url.fragment != "":
241                 link += "#" + url.fragment
242 
243             formatted_content += f'<a href="{potential_link}">{link}</a>'
244         except (ValidationError, UnicodeError):
245             formatted_content += potential_link
246 
247         if wrapped:
248             formatted_content += wrapper_close
249 
250     return formatted_content
251 
252 
253 def _wrapped(text):
254     """check if a line of text is wrapped"""
255     wrappers = [("(", ")"), ("[", "]"), ("{", "}")]
256     for wrapper in wrappers:
257         if text[0] == wrapper[0] and text[-1] == wrapper[-1]:
258             return True
259     return False
260 
261 
262 def to_markdown(content):
263     """catch links and convert to markdown"""
264     content = format_links(content)
265     content = markdown(content)
266     # sanitize resulting html
267     sanitizer = InputHtmlParser()
268     sanitizer.feed(content)
269     return sanitizer.get_output()
270
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py
--- a/bookwyrm/views/status.py
+++ b/bookwyrm/views/status.py
@@ -85,9 +85,7 @@
         if hasattr(status, "quote"):
             status.raw_quote = status.quote
 
-        if not status.sensitive and status.content_warning:
-            # the cw text field remains populated when you click "remove"
-            status.content_warning = None
+        status.sensitive = status.content_warning not in [None, ""]
         status.save(broadcast=False)
 
         # inspect the text for user tags

{"golden_diff": "diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py\n--- a/bookwyrm/views/status.py\n+++ b/bookwyrm/views/status.py\n@@ -85,9 +85,7 @@\n         if hasattr(status, \"quote\"):\n             status.raw_quote = status.quote\n \n-        if not status.sensitive and status.content_warning:\n-            # the cw text field remains populated when you click \"remove\"\n-            status.content_warning = None\n+        status.sensitive = status.content_warning not in [None, \"\"]\n         status.save(broadcast=False)\n \n         # inspect the text for user tags\n", "issue": "Clarify UI around content warnings\nA few problems with content warnings:\r\n - [ ] The button is icon-only and hard to find for visual users in the compose UI (I think it's fine for screen reader users):\r\n<img width=\"360\" alt=\"Screen Shot 2021-09-18 at 11 11 48 AM\" src=\"https://user-images.githubusercontent.com/1807695/133898456-ec77f0d2-febd-428f-aa70-4702e8576d31.png\">\r\n\r\n - [x] ~When content warnings are active in the compose box, they aren't clearly labeled (again, for visual users only):~ (fixed by #1422)\r\n<img width=\"357\" alt=\"Screen Shot 2021-09-18 at 11 12 44 AM\" src=\"https://user-images.githubusercontent.com/1807695/133898461-71879dc4-7a69-4280-a33f-7607746b8454.png\">\r\n\r\n- [x] #1430\r\n<img width=\"779\" alt=\"Screen Shot 2021-09-18 at 11 13 09 AM\" src=\"https://user-images.githubusercontent.com/1807695/133898481-c63000d9-1ffd-4742-89ec-8e0558921c30.png\">\r\n\n", "before_files": [{"content": "\"\"\" what are we here for if not for posting \"\"\"\nimport re\nfrom urllib.parse import urlparse\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.core.validators import URLValidator\nfrom django.core.exceptions import ValidationError\nfrom django.http import HttpResponse, HttpResponseBadRequest, Http404\nfrom django.shortcuts import get_object_or_404, redirect\nfrom django.template.response import TemplateResponse\nfrom django.utils import timezone\nfrom django.utils.decorators import method_decorator\nfrom django.views import View\nfrom django.views.decorators.http import require_POST\n\nfrom markdown import markdown\nfrom bookwyrm import forms, models\nfrom bookwyrm.sanitize_html import InputHtmlParser\nfrom bookwyrm.settings import DOMAIN\nfrom bookwyrm.utils import regex\nfrom .helpers import handle_remote_webfinger, is_api_request\nfrom .helpers import load_date_in_user_tz_as_utc\n\n\n# pylint: disable= no-self-use\n@method_decorator(login_required, name=\"dispatch\")\nclass EditStatus(View):\n    \"\"\"the view for *posting*\"\"\"\n\n    def get(self, request, status_id):  # pylint: disable=unused-argument\n        \"\"\"load the edit panel\"\"\"\n        status = get_object_or_404(\n            models.Status.objects.select_subclasses(), id=status_id\n        )\n        status.raise_not_editable(request.user)\n\n        status_type = \"reply\" if status.reply_parent else status.status_type.lower()\n        data = {\n            \"type\": status_type,\n            \"book\": getattr(status, \"book\", None),\n            \"draft\": status,\n        }\n        return TemplateResponse(request, \"compose.html\", data)\n\n\n# pylint: disable= no-self-use\n@method_decorator(login_required, name=\"dispatch\")\nclass CreateStatus(View):\n    \"\"\"the view for *posting*\"\"\"\n\n    def get(self, request, status_type):  # pylint: disable=unused-argument\n        \"\"\"compose view (...not used?)\"\"\"\n        book = get_object_or_404(models.Edition, id=request.GET.get(\"book\"))\n        data = {\"book\": book}\n        return TemplateResponse(request, \"compose.html\", data)\n\n    # pylint: disable=too-many-branches\n    def post(self, request, status_type, existing_status_id=None):\n        \"\"\"create status of whatever type\"\"\"\n        created = not existing_status_id\n        existing_status = None\n        if existing_status_id:\n            existing_status = get_object_or_404(\n                models.Status.objects.select_subclasses(), id=existing_status_id\n            )\n            existing_status.raise_not_editable(request.user)\n            existing_status.edited_date = timezone.now()\n\n        status_type = status_type[0].upper() + status_type[1:]\n\n        try:\n            form = getattr(forms, f\"{status_type}Form\")(\n                request.POST, instance=existing_status\n            )\n        except AttributeError:\n            return HttpResponseBadRequest()\n        if not form.is_valid():\n            if is_api_request(request):\n                return HttpResponse(status=500)\n            return redirect(request.headers.get(\"Referer\", \"/\"))\n\n        status = form.save(commit=False)\n        # save the plain, unformatted version of the status for future editing\n        status.raw_content = status.content\n        if hasattr(status, \"quote\"):\n            status.raw_quote = status.quote\n\n        if not status.sensitive and status.content_warning:\n            # the cw text field remains populated when you click \"remove\"\n            status.content_warning = None\n        status.save(broadcast=False)\n\n        # inspect the text for user tags\n        content = status.content\n        for (mention_text, mention_user) in find_mentions(content):\n            # add them to status mentions fk\n            status.mention_users.add(mention_user)\n\n            # turn the mention into a link\n            content = re.sub(\n                rf\"{mention_text}([^@]|$)\",\n                rf'<a href=\"{mention_user.remote_id}\">{mention_text}</a>\\g<1>',\n                content,\n            )\n        # add reply parent to mentions\n        if status.reply_parent:\n            status.mention_users.add(status.reply_parent.user)\n\n        # deduplicate mentions\n        status.mention_users.set(set(status.mention_users.all()))\n\n        # don't apply formatting to generated notes\n        if not isinstance(status, models.GeneratedNote) and content:\n            status.content = to_markdown(content)\n        # do apply formatting to quotes\n        if hasattr(status, \"quote\"):\n            status.quote = to_markdown(status.quote)\n\n        status.save(created=created)\n\n        # update a readthrough, if needed\n        if bool(request.POST.get(\"id\")):\n            try:\n                edit_readthrough(request)\n            except Http404:\n                pass\n\n        if is_api_request(request):\n            return HttpResponse()\n        return redirect(\"/\")\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass DeleteStatus(View):\n    \"\"\"tombstone that bad boy\"\"\"\n\n    def post(self, request, status_id):\n        \"\"\"delete and tombstone a status\"\"\"\n        status = get_object_or_404(models.Status, id=status_id)\n\n        # don't let people delete other people's statuses\n        status.raise_not_deletable(request.user)\n\n        # perform deletion\n        status.delete()\n        return redirect(request.headers.get(\"Referer\", \"/\"))\n\n\n@login_required\n@require_POST\ndef update_progress(request, book_id):  # pylint: disable=unused-argument\n    \"\"\"Either it's just a progress update, or it's a comment with a progress update\"\"\"\n    if request.POST.get(\"post-status\"):\n        return CreateStatus.as_view()(request, \"comment\")\n    return edit_readthrough(request)\n\n\n@login_required\n@require_POST\ndef edit_readthrough(request):\n    \"\"\"can't use the form because the dates are too finnicky\"\"\"\n    # TODO: remove this, it duplicates the code in the ReadThrough view\n    readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get(\"id\"))\n    readthrough.raise_not_editable(request.user)\n\n    readthrough.start_date = load_date_in_user_tz_as_utc(\n        request.POST.get(\"start_date\"), request.user\n    )\n    readthrough.finish_date = load_date_in_user_tz_as_utc(\n        request.POST.get(\"finish_date\"), request.user\n    )\n\n    progress = request.POST.get(\"progress\")\n    try:\n        progress = int(progress)\n        readthrough.progress = progress\n    except (ValueError, TypeError):\n        pass\n\n    progress_mode = request.POST.get(\"progress_mode\")\n    try:\n        progress_mode = models.ProgressMode(progress_mode)\n        readthrough.progress_mode = progress_mode\n    except ValueError:\n        pass\n\n    readthrough.save()\n\n    # record the progress update individually\n    # use default now for date field\n    readthrough.create_update()\n\n    if is_api_request(request):\n        return HttpResponse()\n    return redirect(request.headers.get(\"Referer\", \"/\"))\n\n\ndef find_mentions(content):\n    \"\"\"detect @mentions in raw status content\"\"\"\n    if not content:\n        return\n    for match in re.finditer(regex.STRICT_USERNAME, content):\n        username = match.group().strip().split(\"@\")[1:]\n        if len(username) == 1:\n            # this looks like a local user (@user), fill in the domain\n            username.append(DOMAIN)\n        username = \"@\".join(username)\n\n        mention_user = handle_remote_webfinger(username)\n        if not mention_user:\n            # we can ignore users we don't know about\n            continue\n        yield (match.group(), mention_user)\n\n\ndef format_links(content):\n    \"\"\"detect and format links\"\"\"\n    validator = URLValidator()\n    formatted_content = \"\"\n    split_content = re.split(r\"(\\s+)\", content)\n\n    for potential_link in split_content:\n        if not potential_link:\n            continue\n        wrapped = _wrapped(potential_link)\n        if wrapped:\n            wrapper_close = potential_link[-1]\n            formatted_content += potential_link[0]\n            potential_link = potential_link[1:-1]\n\n        try:\n            # raises an error on anything that's not a valid link\n            validator(potential_link)\n\n            # use everything but the scheme in the presentation of the link\n            url = urlparse(potential_link)\n            link = url.netloc + url.path + url.params\n            if url.query != \"\":\n                link += \"?\" + url.query\n            if url.fragment != \"\":\n                link += \"#\" + url.fragment\n\n            formatted_content += f'<a href=\"{potential_link}\">{link}</a>'\n        except (ValidationError, UnicodeError):\n            formatted_content += potential_link\n\n        if wrapped:\n            formatted_content += wrapper_close\n\n    return formatted_content\n\n\ndef _wrapped(text):\n    \"\"\"check if a line of text is wrapped\"\"\"\n    wrappers = [(\"(\", \")\"), (\"[\", \"]\"), (\"{\", \"}\")]\n    for wrapper in wrappers:\n        if text[0] == wrapper[0] and text[-1] == wrapper[-1]:\n            return True\n    return False\n\n\ndef to_markdown(content):\n    \"\"\"catch links and convert to markdown\"\"\"\n    content = format_links(content)\n    content = markdown(content)\n    # sanitize resulting html\n    sanitizer = InputHtmlParser()\n    sanitizer.feed(content)\n    return sanitizer.get_output()\n", "path": "bookwyrm/views/status.py"}], "after_files": [{"content": "\"\"\" what are we here for if not for posting \"\"\"\nimport re\nfrom urllib.parse import urlparse\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.core.validators import URLValidator\nfrom django.core.exceptions import ValidationError\nfrom django.http import HttpResponse, HttpResponseBadRequest, Http404\nfrom django.shortcuts import get_object_or_404, redirect\nfrom django.template.response import TemplateResponse\nfrom django.utils import timezone\nfrom django.utils.decorators import method_decorator\nfrom django.views import View\nfrom django.views.decorators.http import require_POST\n\nfrom markdown import markdown\nfrom bookwyrm import forms, models\nfrom bookwyrm.sanitize_html import InputHtmlParser\nfrom bookwyrm.settings import DOMAIN\nfrom bookwyrm.utils import regex\nfrom .helpers import handle_remote_webfinger, is_api_request\nfrom .helpers import load_date_in_user_tz_as_utc\n\n\n# pylint: disable= no-self-use\n@method_decorator(login_required, name=\"dispatch\")\nclass EditStatus(View):\n    \"\"\"the view for *posting*\"\"\"\n\n    def get(self, request, status_id):  # pylint: disable=unused-argument\n        \"\"\"load the edit panel\"\"\"\n        status = get_object_or_404(\n            models.Status.objects.select_subclasses(), id=status_id\n        )\n        status.raise_not_editable(request.user)\n\n        status_type = \"reply\" if status.reply_parent else status.status_type.lower()\n        data = {\n            \"type\": status_type,\n            \"book\": getattr(status, \"book\", None),\n            \"draft\": status,\n        }\n        return TemplateResponse(request, \"compose.html\", data)\n\n\n# pylint: disable= no-self-use\n@method_decorator(login_required, name=\"dispatch\")\nclass CreateStatus(View):\n    \"\"\"the view for *posting*\"\"\"\n\n    def get(self, request, status_type):  # pylint: disable=unused-argument\n        \"\"\"compose view (...not used?)\"\"\"\n        book = get_object_or_404(models.Edition, id=request.GET.get(\"book\"))\n        data = {\"book\": book}\n        return TemplateResponse(request, \"compose.html\", data)\n\n    # pylint: disable=too-many-branches\n    def post(self, request, status_type, existing_status_id=None):\n        \"\"\"create status of whatever type\"\"\"\n        created = not existing_status_id\n        existing_status = None\n        if existing_status_id:\n            existing_status = get_object_or_404(\n                models.Status.objects.select_subclasses(), id=existing_status_id\n            )\n            existing_status.raise_not_editable(request.user)\n            existing_status.edited_date = timezone.now()\n\n        status_type = status_type[0].upper() + status_type[1:]\n\n        try:\n            form = getattr(forms, f\"{status_type}Form\")(\n                request.POST, instance=existing_status\n            )\n        except AttributeError:\n            return HttpResponseBadRequest()\n        if not form.is_valid():\n            if is_api_request(request):\n                return HttpResponse(status=500)\n            return redirect(request.headers.get(\"Referer\", \"/\"))\n\n        status = form.save(commit=False)\n        # save the plain, unformatted version of the status for future editing\n        status.raw_content = status.content\n        if hasattr(status, \"quote\"):\n            status.raw_quote = status.quote\n\n        status.sensitive = status.content_warning not in [None, \"\"]\n        status.save(broadcast=False)\n\n        # inspect the text for user tags\n        content = status.content\n        for (mention_text, mention_user) in find_mentions(content):\n            # add them to status mentions fk\n            status.mention_users.add(mention_user)\n\n            # turn the mention into a link\n            content = re.sub(\n                rf\"{mention_text}([^@]|$)\",\n                rf'<a href=\"{mention_user.remote_id}\">{mention_text}</a>\\g<1>',\n                content,\n            )\n        # add reply parent to mentions\n        if status.reply_parent:\n            status.mention_users.add(status.reply_parent.user)\n\n        # deduplicate mentions\n        status.mention_users.set(set(status.mention_users.all()))\n\n        # don't apply formatting to generated notes\n        if not isinstance(status, models.GeneratedNote) and content:\n            status.content = to_markdown(content)\n        # do apply formatting to quotes\n        if hasattr(status, \"quote\"):\n            status.quote = to_markdown(status.quote)\n\n        status.save(created=created)\n\n        # update a readthrough, if needed\n        if bool(request.POST.get(\"id\")):\n            try:\n                edit_readthrough(request)\n            except Http404:\n                pass\n\n        if is_api_request(request):\n            return HttpResponse()\n        return redirect(\"/\")\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass DeleteStatus(View):\n    \"\"\"tombstone that bad boy\"\"\"\n\n    def post(self, request, status_id):\n        \"\"\"delete and tombstone a status\"\"\"\n        status = get_object_or_404(models.Status, id=status_id)\n\n        # don't let people delete other people's statuses\n        status.raise_not_deletable(request.user)\n\n        # perform deletion\n        status.delete()\n        return redirect(request.headers.get(\"Referer\", \"/\"))\n\n\n@login_required\n@require_POST\ndef update_progress(request, book_id):  # pylint: disable=unused-argument\n    \"\"\"Either it's just a progress update, or it's a comment with a progress update\"\"\"\n    if request.POST.get(\"post-status\"):\n        return CreateStatus.as_view()(request, \"comment\")\n    return edit_readthrough(request)\n\n\n@login_required\n@require_POST\ndef edit_readthrough(request):\n    \"\"\"can't use the form because the dates are too finnicky\"\"\"\n    # TODO: remove this, it duplicates the code in the ReadThrough view\n    readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get(\"id\"))\n    readthrough.raise_not_editable(request.user)\n\n    readthrough.start_date = load_date_in_user_tz_as_utc(\n        request.POST.get(\"start_date\"), request.user\n    )\n    readthrough.finish_date = load_date_in_user_tz_as_utc(\n        request.POST.get(\"finish_date\"), request.user\n    )\n\n    progress = request.POST.get(\"progress\")\n    try:\n        progress = int(progress)\n        readthrough.progress = progress\n    except (ValueError, TypeError):\n        pass\n\n    progress_mode = request.POST.get(\"progress_mode\")\n    try:\n        progress_mode = models.ProgressMode(progress_mode)\n        readthrough.progress_mode = progress_mode\n    except ValueError:\n        pass\n\n    readthrough.save()\n\n    # record the progress update individually\n    # use default now for date field\n    readthrough.create_update()\n\n    if is_api_request(request):\n        return HttpResponse()\n    return redirect(request.headers.get(\"Referer\", \"/\"))\n\n\ndef find_mentions(content):\n    \"\"\"detect @mentions in raw status content\"\"\"\n    if not content:\n        return\n    for match in re.finditer(regex.STRICT_USERNAME, content):\n        username = match.group().strip().split(\"@\")[1:]\n        if len(username) == 1:\n            # this looks like a local user (@user), fill in the domain\n            username.append(DOMAIN)\n        username = \"@\".join(username)\n\n        mention_user = handle_remote_webfinger(username)\n        if not mention_user:\n            # we can ignore users we don't know about\n            continue\n        yield (match.group(), mention_user)\n\n\ndef format_links(content):\n    \"\"\"detect and format links\"\"\"\n    validator = URLValidator()\n    formatted_content = \"\"\n    split_content = re.split(r\"(\\s+)\", content)\n\n    for potential_link in split_content:\n        if not potential_link:\n            continue\n        wrapped = _wrapped(potential_link)\n        if wrapped:\n            wrapper_close = potential_link[-1]\n            formatted_content += potential_link[0]\n            potential_link = potential_link[1:-1]\n\n        try:\n            # raises an error on anything that's not a valid link\n            validator(potential_link)\n\n            # use everything but the scheme in the presentation of the link\n            url = urlparse(potential_link)\n            link = url.netloc + url.path + url.params\n            if url.query != \"\":\n                link += \"?\" + url.query\n            if url.fragment != \"\":\n                link += \"#\" + url.fragment\n\n            formatted_content += f'<a href=\"{potential_link}\">{link}</a>'\n        except (ValidationError, UnicodeError):\n            formatted_content += potential_link\n\n        if wrapped:\n            formatted_content += wrapper_close\n\n    return formatted_content\n\n\ndef _wrapped(text):\n    \"\"\"check if a line of text is wrapped\"\"\"\n    wrappers = [(\"(\", \")\"), (\"[\", \"]\"), (\"{\", \"}\")]\n    for wrapper in wrappers:\n        if text[0] == wrapper[0] and text[-1] == wrapper[-1]:\n            return True\n    return False\n\n\ndef to_markdown(content):\n    \"\"\"catch links and convert to markdown\"\"\"\n    content = format_links(content)\n    content = markdown(content)\n    # sanitize resulting html\n    sanitizer = InputHtmlParser()\n    sanitizer.feed(content)\n    return sanitizer.get_output()\n", "path": "bookwyrm/views/status.py"}]}