mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-17 05:36:34 +00:00
commit
1e41458a44
23 changed files with 268 additions and 154 deletions
|
@ -35,6 +35,7 @@ class Note(ActivityObject):
|
||||||
tag: List[Link] = field(default_factory=lambda: [])
|
tag: List[Link] = field(default_factory=lambda: [])
|
||||||
attachment: List[Document] = field(default_factory=lambda: [])
|
attachment: List[Document] = field(default_factory=lambda: [])
|
||||||
sensitive: bool = False
|
sensitive: bool = False
|
||||||
|
updated: str = None
|
||||||
type: str = "Note"
|
type: str = "Note"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,8 @@ class Update(Verb):
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
"""update a model instance from the dataclass"""
|
"""update a model instance from the dataclass"""
|
||||||
if self.object:
|
if not self.object:
|
||||||
|
return
|
||||||
self.object.to_model(allow_create=False)
|
self.object.to_model(allow_create=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
19
bookwyrm/migrations/0109_status_edited_date.py
Normal file
19
bookwyrm/migrations/0109_status_edited_date.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-10-15 15:54
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0108_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="status",
|
||||||
|
name="edited_date",
|
||||||
|
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -43,6 +43,9 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
published_date = fields.DateTimeField(
|
published_date = fields.DateTimeField(
|
||||||
default=timezone.now, activitypub_field="published"
|
default=timezone.now, activitypub_field="published"
|
||||||
)
|
)
|
||||||
|
edited_date = fields.DateTimeField(
|
||||||
|
blank=True, null=True, activitypub_field="updated"
|
||||||
|
)
|
||||||
deleted = models.BooleanField(default=False)
|
deleted = models.BooleanField(default=False)
|
||||||
deleted_date = models.DateTimeField(blank=True, null=True)
|
deleted_date = models.DateTimeField(blank=True, null=True)
|
||||||
favorites = models.ManyToManyField(
|
favorites = models.ManyToManyField(
|
||||||
|
|
|
@ -9,12 +9,12 @@ from django.utils.translation import gettext_lazy as _
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
DOMAIN = env("DOMAIN")
|
DOMAIN = env("DOMAIN")
|
||||||
VERSION = "0.0.1"
|
VERSION = "0.1.0"
|
||||||
|
|
||||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||||
|
|
||||||
JS_CACHE = "c02929b1"
|
JS_CACHE = "3eb4edb1"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
|
|
@ -509,6 +509,20 @@ ol.ordered-list li::before {
|
||||||
border-left: 2px solid #e0e0e0;
|
border-left: 2px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Breadcrumbs
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.breadcrumb li:first-child * {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb li > * {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dimensions
|
/* Dimensions
|
||||||
* @todo These could be in rem.
|
* @todo These could be in rem.
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
|
@ -35,7 +35,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||||
size="3"
|
size="3"
|
||||||
value="{% firstof draft.progress readthrough.progress '' %}"
|
value="{% firstof draft.progress readthrough.progress '' %}"
|
||||||
id="progress_{{ uuid }}"
|
id="progress_{{ uuid }}"
|
||||||
data-cache-draft="id_progress_comment_{{ book.id }}"
|
{% if not draft %}data-cache-draft="id_progress_comment_{{ book.id }}"{% endif %}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
@ -43,7 +43,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||||
<select
|
<select
|
||||||
name="progress_mode"
|
name="progress_mode"
|
||||||
aria-label="Progress mode"
|
aria-label="Progress mode"
|
||||||
data-cache-draft="id_progress_mode_comment_{{ book.id }}"
|
{% if not draft %}data-cache-draft="id_progress_mode_comment_{{ book.id }}"{% endif %}
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
value="PG"
|
value="PG"
|
||||||
|
|
|
@ -11,7 +11,7 @@ draft: an existing Status object that is providing default values for input fiel
|
||||||
<textarea
|
<textarea
|
||||||
name="content"
|
name="content"
|
||||||
class="textarea save-draft"
|
class="textarea save-draft"
|
||||||
data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
{% if not draft %}data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"{% endif %}
|
||||||
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
|
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
|
||||||
placeholder="{{ placeholder }}"
|
placeholder="{{ placeholder }}"
|
||||||
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
|
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
id="id_content_warning_{{ uuid }}{{ local_uuid }}"
|
id="id_content_warning_{{ uuid }}{{ local_uuid }}"
|
||||||
placeholder="{% trans 'Spoilers ahead!' %}"
|
placeholder="{% trans 'Spoilers ahead!' %}"
|
||||||
value="{% firstof draft.content_warning reply_parent.content_warning '' %}"
|
value="{% firstof draft.content_warning reply_parent.content_warning '' %}"
|
||||||
data-cache-draft="id_content_warning_{{ book.id }}_{{ type }}"
|
{% if not draft %}data-cache-draft="id_content_warning_{{ book.id }}_{{ type }}"{% endif %}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
id="id_show_spoilers_{{ uuid }}{{ local_uuid }}"
|
id="id_show_spoilers_{{ uuid }}{{ local_uuid }}"
|
||||||
{% if draft.content_warning or status.content_warning %}checked{% endif %}
|
{% if draft.content_warning or status.content_warning %}checked{% endif %}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
data-cache-draft="id_sensitive_{{ book.id }}_{{ type }}{{ reply_parent.id }}"
|
{% if not draft %}data-cache-draft="id_sensitive_{{ book.id }}_{{ type }}{{ reply_parent.id }}"{% endif %}
|
||||||
>
|
>
|
||||||
{% trans "Include spoiler alert" as button_text %}
|
{% trans "Include spoiler alert" as button_text %}
|
||||||
{% firstof draft.content_warning status.content_warning as pressed %}
|
{% firstof draft.content_warning status.content_warning as pressed %}
|
||||||
|
|
|
@ -17,7 +17,11 @@ reply_parent: the Status object this post will be in reply to, if applicable
|
||||||
<form
|
<form
|
||||||
class="is-flex-grow-1{% if not no_script %} submit-status{% endif %}"
|
class="is-flex-grow-1{% if not no_script %} submit-status{% endif %}"
|
||||||
name="{{ type }}"
|
name="{{ type }}"
|
||||||
action="/post/{{ type }}"
|
{% if draft %}
|
||||||
|
action="{% url 'create-status' type draft.id %}"
|
||||||
|
{% else %}
|
||||||
|
action="{% url 'create-status' type %}"
|
||||||
|
{% endif %}
|
||||||
method="post"
|
method="post"
|
||||||
id="form_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
id="form_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
||||||
>
|
>
|
||||||
|
|
|
@ -24,7 +24,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||||
id="id_quote_{{ book.id }}_{{ type }}"
|
id="id_quote_{{ book.id }}_{{ type }}"
|
||||||
placeholder="{% blocktrans with book_title=book.title %}An excerpt from '{{ book_title }}'{% endblocktrans %}"
|
placeholder="{% blocktrans with book_title=book.title %}An excerpt from '{{ book_title }}'{% endblocktrans %}"
|
||||||
required
|
required
|
||||||
data-cache-draft="id_quote_{{ book.id }}_{{ type }}"
|
{% if not draft %}data-cache-draft="id_quote_{{ book.id }}_{{ type }}"{% endif %}
|
||||||
>{{ draft.quote|default:'' }}</textarea>
|
>{{ draft.quote|default:'' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||||
<select
|
<select
|
||||||
name="position_mode"
|
name="position_mode"
|
||||||
aria-label="Position mode"
|
aria-label="Position mode"
|
||||||
data-cache-draft="id_position_mode_{{ book.id }}_{{ type }}"
|
{% if not draft %}data-cache-draft="id_position_mode_{{ book.id }}_{{ type }}"{% endif %}
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
value="PG"
|
value="PG"
|
||||||
|
@ -63,7 +63,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||||
size="3"
|
size="3"
|
||||||
value="{% firstof draft.position '' %}"
|
value="{% firstof draft.position '' %}"
|
||||||
id="position_{{ uuid }}"
|
id="position_{{ uuid }}"
|
||||||
data-cache-draft="id_position_{{ book.id }}_{{ type }}"
|
{% if not draft %}data-cache-draft="id_position_{{ book.id }}_{{ type }}"{% endif %}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||||
id="id_name_{{ book.id }}"
|
id="id_name_{{ book.id }}"
|
||||||
placeholder="{% blocktrans with book_title=book.title %}Your review of '{{ book_title }}'{% endblocktrans %}"
|
placeholder="{% blocktrans with book_title=book.title %}Your review of '{{ book_title }}'{% endblocktrans %}"
|
||||||
value="{% firstof draft.name ''%}"
|
value="{% firstof draft.name ''%}"
|
||||||
data-cache-draft="id_name_{{ book.id }}_{{ type }}"
|
{% if not draft %}data-cache-draft="id_name_{{ book.id }}_{{ type }}"{% endif %}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,19 @@
|
||||||
|
{% spaceless %}
|
||||||
|
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if total_pages %}
|
{% if total_pages %}
|
||||||
{% blocktrans with page=page|intcomma total_pages=total_pages|intcomma %}page {{ page }} of {{ total_pages }}{% endblocktrans %}
|
|
||||||
|
{% blocktrans trimmed with page=page|intcomma total_pages=total_pages|intcomma %}
|
||||||
|
page {{ page }} of {{ total_pages }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% blocktrans with page=page|intcomma %}page {{ page }}{% endblocktrans %}
|
|
||||||
|
{% blocktrans trimmed with page=page|intcomma %}
|
||||||
|
page {{ page }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endspaceless %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<span class="is-sr-only">{% trans "Leave a rating" %}</span>
|
<span class="is-sr-only">{% trans "Leave a rating" %}</span>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<form class="hidden-form" name="rate" action="/post/rating" method="POST">
|
<form class="hidden-form" name="rate" action="{% url 'create-status' 'rating' %}" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
|
|
|
@ -31,18 +31,38 @@
|
||||||
|
|
||||||
{% include "snippets/status/header_content.html" %}
|
{% include "snippets/status/header_content.html" %}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="is-size-7 is-flex is-align-items-center">
|
<div class="breadcrumb has-dot-separator is-small">
|
||||||
<a href="{{ status.remote_id }}{% if status.user.local %}#anchor-{{ status.id }}{% endif %}">{{ status.published_date|published_date }}</a>
|
<ul class="is-flex is-align-items-center">
|
||||||
|
<li>
|
||||||
|
<a href="{{ status.remote_id }}{% if status.user.local %}#anchor-{{ status.id }}{% endif %}">
|
||||||
|
{{ status.published_date|published_date }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if status.edited_date %}
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
{% blocktrans with date=status.edited_date|published_date %}edited {{ date }}{% endblocktrans %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if status.progress %}
|
{% if status.progress %}
|
||||||
<span class="ml-1">
|
<li class="ml-1">
|
||||||
|
<span>
|
||||||
{% if status.progress_mode == 'PG' %}
|
{% if status.progress_mode == 'PG' %}
|
||||||
({% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %})
|
{% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %}
|
||||||
{% else %}
|
{% else %}
|
||||||
({{ status.progress }}%)
|
{{ status.progress }}%
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<li>
|
||||||
{% include 'snippets/privacy-icons.html' with item=status %}
|
{% include 'snippets/privacy-icons.html' with item=status %}
|
||||||
</p>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card-bonus %}
|
{% block card-bonus %}
|
||||||
{% if request.user.is_authenticated and not moderation_mode %}
|
{% if request.user.is_authenticated and not moderation_mode and not no_interact %}
|
||||||
{% with status.id|uuid as uuid %}
|
{% with status.id|uuid as uuid %}
|
||||||
<section class="reply-panel is-hidden" id="show_comment_{{ status.id }}">
|
<section class="reply-panel is-hidden" id="show_comment_{{ status.id }}">
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|
|
@ -20,12 +20,9 @@
|
||||||
</li>
|
</li>
|
||||||
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
|
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
|
||||||
<li role="menuitem" class="dropdown-item p-0">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<form class="" name="delete-{{ status.id }}" action="{% url 'redraft' status.id %}" method="post">
|
<a href="{% url 'edit-status' status.id %}" class="button is-radiusless is-fullwidth is-small" type="submit">
|
||||||
{% csrf_token %}
|
{% trans "Edit" %}
|
||||||
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
|
</a>
|
||||||
{% trans "Delete & re-draft" %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -37,9 +37,9 @@ class InboxUpdate(TestCase):
|
||||||
outbox="https://example.com/users/rat/outbox",
|
outbox="https://example.com/users/rat/outbox",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.create_json = {
|
self.update_json = {
|
||||||
"id": "hi",
|
"id": "hi",
|
||||||
"type": "Create",
|
"type": "Update",
|
||||||
"actor": "hi",
|
"actor": "hi",
|
||||||
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||||
"cc": ["https://example.com/user/mouse/followers"],
|
"cc": ["https://example.com/user/mouse/followers"],
|
||||||
|
@ -54,13 +54,8 @@ class InboxUpdate(TestCase):
|
||||||
book_list = models.List.objects.create(
|
book_list = models.List.objects.create(
|
||||||
name="hi", remote_id="https://example.com/list/22", user=self.local_user
|
name="hi", remote_id="https://example.com/list/22", user=self.local_user
|
||||||
)
|
)
|
||||||
activity = {
|
activity = self.update_json
|
||||||
"type": "Update",
|
activity["object"] = {
|
||||||
"to": [],
|
|
||||||
"cc": [],
|
|
||||||
"actor": "hi",
|
|
||||||
"id": "sdkjf",
|
|
||||||
"object": {
|
|
||||||
"id": "https://example.com/list/22",
|
"id": "https://example.com/list/22",
|
||||||
"type": "BookList",
|
"type": "BookList",
|
||||||
"totalItems": 1,
|
"totalItems": 1,
|
||||||
|
@ -73,7 +68,6 @@ class InboxUpdate(TestCase):
|
||||||
"summary": "summary text",
|
"summary": "summary text",
|
||||||
"curation": "curated",
|
"curation": "curated",
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
},
|
|
||||||
}
|
}
|
||||||
views.inbox.activity_task(activity)
|
views.inbox.activity_task(activity)
|
||||||
book_list.refresh_from_db()
|
book_list.refresh_from_db()
|
||||||
|
@ -176,3 +170,26 @@ class InboxUpdate(TestCase):
|
||||||
)
|
)
|
||||||
book = models.Work.objects.get(id=book.id)
|
book = models.Work.objects.get(id=book.id)
|
||||||
self.assertEqual(book.title, "Piranesi")
|
self.assertEqual(book.title, "Piranesi")
|
||||||
|
|
||||||
|
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||||
|
def test_update_status(self, *_):
|
||||||
|
"""edit a status"""
|
||||||
|
status = models.Status.objects.create(user=self.remote_user, content="hi")
|
||||||
|
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json")
|
||||||
|
status_data = json.loads(datafile.read_bytes())
|
||||||
|
status_data["id"] = status.remote_id
|
||||||
|
status_data["updated"] = "2021-12-13T05:09:29Z"
|
||||||
|
|
||||||
|
activity = self.update_json
|
||||||
|
activity["object"] = status_data
|
||||||
|
|
||||||
|
with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"):
|
||||||
|
views.inbox.activity_task(activity)
|
||||||
|
|
||||||
|
status.refresh_from_db()
|
||||||
|
self.assertEqual(status.content, "test content in note")
|
||||||
|
self.assertEqual(status.edited_date.year, 2021)
|
||||||
|
self.assertEqual(status.edited_date.month, 12)
|
||||||
|
self.assertEqual(status.edited_date.day, 13)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookwyrm import forms, models, views
|
from bookwyrm import forms, models, views
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
from bookwyrm.tests.validate_html import validate_html
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
@ -70,6 +71,7 @@ class StatusViews(TestCase):
|
||||||
self.assertEqual(status.content, "<p>hi</p>")
|
self.assertEqual(status.content, "<p>hi</p>")
|
||||||
self.assertEqual(status.user, self.local_user)
|
self.assertEqual(status.user, self.local_user)
|
||||||
self.assertEqual(status.book, self.book)
|
self.assertEqual(status.book, self.book)
|
||||||
|
self.assertIsNone(status.edited_date)
|
||||||
|
|
||||||
def test_handle_status_reply(self, *_):
|
def test_handle_status_reply(self, *_):
|
||||||
"""create a status in reply to an existing status"""
|
"""create a status in reply to an existing status"""
|
||||||
|
@ -168,60 +170,6 @@ class StatusViews(TestCase):
|
||||||
self.assertFalse(self.remote_user in reply.mention_users.all())
|
self.assertFalse(self.remote_user in reply.mention_users.all())
|
||||||
self.assertTrue(self.local_user in reply.mention_users.all())
|
self.assertTrue(self.local_user in reply.mention_users.all())
|
||||||
|
|
||||||
def test_delete_and_redraft(self, *_):
|
|
||||||
"""delete and re-draft a status"""
|
|
||||||
view = views.DeleteAndRedraft.as_view()
|
|
||||||
request = self.factory.post("")
|
|
||||||
request.user = self.local_user
|
|
||||||
status = models.Comment.objects.create(
|
|
||||||
content="hi", book=self.book, user=self.local_user
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock:
|
|
||||||
result = view(request, status.id)
|
|
||||||
self.assertTrue(mock.called)
|
|
||||||
result.render()
|
|
||||||
|
|
||||||
# make sure it was deleted
|
|
||||||
status.refresh_from_db()
|
|
||||||
self.assertTrue(status.deleted)
|
|
||||||
|
|
||||||
def test_delete_and_redraft_invalid_status_type_rating(self, *_):
|
|
||||||
"""you can't redraft generated statuses"""
|
|
||||||
view = views.DeleteAndRedraft.as_view()
|
|
||||||
request = self.factory.post("")
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch("bookwyrm.activitystreams.add_status_task.delay"):
|
|
||||||
status = models.ReviewRating.objects.create(
|
|
||||||
book=self.book, rating=2.0, user=self.local_user
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock:
|
|
||||||
with self.assertRaises(PermissionDenied):
|
|
||||||
view(request, status.id)
|
|
||||||
self.assertFalse(mock.called)
|
|
||||||
|
|
||||||
status.refresh_from_db()
|
|
||||||
self.assertFalse(status.deleted)
|
|
||||||
|
|
||||||
def test_delete_and_redraft_invalid_status_type_generated_note(self, *_):
|
|
||||||
"""you can't redraft generated statuses"""
|
|
||||||
view = views.DeleteAndRedraft.as_view()
|
|
||||||
request = self.factory.post("")
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch("bookwyrm.activitystreams.add_status_task.delay"):
|
|
||||||
status = models.GeneratedNote.objects.create(
|
|
||||||
content="hi", user=self.local_user
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock:
|
|
||||||
with self.assertRaises(PermissionDenied):
|
|
||||||
view(request, status.id)
|
|
||||||
self.assertFalse(mock.called)
|
|
||||||
|
|
||||||
status.refresh_from_db()
|
|
||||||
self.assertFalse(status.deleted)
|
|
||||||
|
|
||||||
def test_find_mentions(self, *_):
|
def test_find_mentions(self, *_):
|
||||||
"""detect and look up @ mentions of users"""
|
"""detect and look up @ mentions of users"""
|
||||||
user = models.User.objects.create_user(
|
user = models.User.objects.create_user(
|
||||||
|
@ -400,3 +348,75 @@ http://www.fish.com/"""
|
||||||
self.assertEqual(activity["object"]["type"], "Tombstone")
|
self.assertEqual(activity["object"]["type"], "Tombstone")
|
||||||
status.refresh_from_db()
|
status.refresh_from_db()
|
||||||
self.assertTrue(status.deleted)
|
self.assertTrue(status.deleted)
|
||||||
|
|
||||||
|
def test_edit_status_get(self, *_):
|
||||||
|
"""load the edit status view"""
|
||||||
|
view = views.EditStatus.as_view()
|
||||||
|
status = models.Comment.objects.create(
|
||||||
|
content="status", user=self.local_user, book=self.book
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request, status.id)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_edit_status_get_reply(self, *_):
|
||||||
|
"""load the edit status view"""
|
||||||
|
view = views.EditStatus.as_view()
|
||||||
|
parent = models.Comment.objects.create(
|
||||||
|
content="parent status", user=self.local_user, book=self.book
|
||||||
|
)
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
content="reply", user=self.local_user, reply_parent=parent
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request, status.id)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_create_status_edit_success(self, mock, *_):
|
||||||
|
"""update an existing status"""
|
||||||
|
status = models.Status.objects.create(content="status", user=self.local_user)
|
||||||
|
self.assertIsNone(status.edited_date)
|
||||||
|
view = views.CreateStatus.as_view()
|
||||||
|
form = forms.CommentForm(
|
||||||
|
{
|
||||||
|
"content": "hi",
|
||||||
|
"user": self.local_user.id,
|
||||||
|
"book": self.book.id,
|
||||||
|
"privacy": "public",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
request = self.factory.post("", form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
view(request, "comment", existing_status_id=status.id)
|
||||||
|
activity = json.loads(mock.call_args_list[1][0][1])
|
||||||
|
self.assertEqual(activity["type"], "Update")
|
||||||
|
self.assertEqual(activity["object"]["id"], status.remote_id)
|
||||||
|
|
||||||
|
status.refresh_from_db()
|
||||||
|
self.assertEqual(status.content, "<p>hi</p>")
|
||||||
|
self.assertIsNotNone(status.edited_date)
|
||||||
|
|
||||||
|
def test_create_status_edit_permission_denied(self, *_):
|
||||||
|
"""update an existing status"""
|
||||||
|
status = models.Status.objects.create(content="status", user=self.local_user)
|
||||||
|
view = views.CreateStatus.as_view()
|
||||||
|
form = forms.CommentForm(
|
||||||
|
{
|
||||||
|
"content": "hi",
|
||||||
|
"user": self.local_user.id,
|
||||||
|
"book": self.book.id,
|
||||||
|
"privacy": "public",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
request = self.factory.post("", form.data)
|
||||||
|
request.user = self.remote_user
|
||||||
|
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
view(request, "comment", existing_status_id=status.id)
|
||||||
|
|
|
@ -315,6 +315,9 @@ urlpatterns = [
|
||||||
re_path(
|
re_path(
|
||||||
rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies"
|
rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies"
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^edit/(?P<status_id>\d+)/?$", views.EditStatus.as_view(), name="edit-status"
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^post/?$",
|
r"^post/?$",
|
||||||
views.CreateStatus.as_view(),
|
views.CreateStatus.as_view(),
|
||||||
|
@ -325,16 +328,16 @@ urlpatterns = [
|
||||||
views.CreateStatus.as_view(),
|
views.CreateStatus.as_view(),
|
||||||
name="create-status",
|
name="create-status",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^post/(?P<status_type>\w+)/(?P<existing_status_id>\d+)/?$",
|
||||||
|
views.CreateStatus.as_view(),
|
||||||
|
name="create-status",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^delete-status/(?P<status_id>\d+)/?$",
|
r"^delete-status/(?P<status_id>\d+)/?$",
|
||||||
views.DeleteStatus.as_view(),
|
views.DeleteStatus.as_view(),
|
||||||
name="delete-status",
|
name="delete-status",
|
||||||
),
|
),
|
||||||
re_path(
|
|
||||||
r"^redraft-status/(?P<status_id>\d+)/?$",
|
|
||||||
views.DeleteAndRedraft.as_view(),
|
|
||||||
name="redraft",
|
|
||||||
),
|
|
||||||
# interact
|
# interact
|
||||||
re_path(r"^favorite/(?P<status_id>\d+)/?$", views.Favorite.as_view(), name="fav"),
|
re_path(r"^favorite/(?P<status_id>\d+)/?$", views.Favorite.as_view(), name="fav"),
|
||||||
re_path(
|
re_path(
|
||||||
|
|
|
@ -60,7 +60,7 @@ from .search import Search
|
||||||
from .shelf import Shelf
|
from .shelf import Shelf
|
||||||
from .shelf import create_shelf, delete_shelf
|
from .shelf import create_shelf, delete_shelf
|
||||||
from .shelf import shelve, unshelve
|
from .shelf import shelve, unshelve
|
||||||
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft, update_progress
|
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress
|
||||||
from .status import edit_readthrough
|
from .status import edit_readthrough
|
||||||
from .updates import get_notification_count, get_unread_status_count
|
from .updates import get_notification_count, get_unread_status_count
|
||||||
from .user import User, Followers, Following, hide_suggestions
|
from .user import User, Followers, Following, hide_suggestions
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.core.exceptions import ValidationError
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, Http404
|
from django.http import HttpResponse, HttpResponseBadRequest, Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
@ -21,23 +22,55 @@ from .helpers import handle_remote_webfinger, is_api_request
|
||||||
from .helpers import load_date_in_user_tz_as_utc
|
from .helpers import load_date_in_user_tz_as_utc
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
@method_decorator(login_required, name="dispatch")
|
||||||
|
class EditStatus(View):
|
||||||
|
"""the view for *posting*"""
|
||||||
|
|
||||||
|
def get(self, request, status_id): # pylint: disable=unused-argument
|
||||||
|
"""load the edit panel"""
|
||||||
|
status = get_object_or_404(
|
||||||
|
models.Status.objects.select_subclasses(), id=status_id
|
||||||
|
)
|
||||||
|
status.raise_not_editable(request.user)
|
||||||
|
|
||||||
|
status_type = "reply" if status.reply_parent else status.status_type.lower()
|
||||||
|
data = {
|
||||||
|
"type": status_type,
|
||||||
|
"book": getattr(status, "book", None),
|
||||||
|
"draft": status,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "compose.html", data)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable= no-self-use
|
# pylint: disable= no-self-use
|
||||||
@method_decorator(login_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
class CreateStatus(View):
|
class CreateStatus(View):
|
||||||
"""the view for *posting*"""
|
"""the view for *posting*"""
|
||||||
|
|
||||||
def get(self, request, status_type): # pylint: disable=unused-argument
|
def get(self, request, status_type): # pylint: disable=unused-argument
|
||||||
"""compose view (used for delete-and-redraft)"""
|
"""compose view (...not used?)"""
|
||||||
book = get_object_or_404(models.Edition, id=request.GET.get("book"))
|
book = get_object_or_404(models.Edition, id=request.GET.get("book"))
|
||||||
data = {"book": book}
|
data = {"book": book}
|
||||||
return TemplateResponse(request, "compose.html", data)
|
return TemplateResponse(request, "compose.html", data)
|
||||||
|
|
||||||
def post(self, request, status_type):
|
def post(self, request, status_type, existing_status_id=None):
|
||||||
"""create status of whatever type"""
|
"""create status of whatever type"""
|
||||||
|
created = not existing_status_id
|
||||||
|
existing_status = None
|
||||||
|
if existing_status_id:
|
||||||
|
existing_status = get_object_or_404(
|
||||||
|
models.Status.objects.select_subclasses(), id=existing_status_id
|
||||||
|
)
|
||||||
|
existing_status.raise_not_editable(request.user)
|
||||||
|
existing_status.edited_date = timezone.now()
|
||||||
|
|
||||||
status_type = status_type[0].upper() + status_type[1:]
|
status_type = status_type[0].upper() + status_type[1:]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
form = getattr(forms, f"{status_type}Form")(request.POST)
|
form = getattr(forms, f"{status_type}Form")(
|
||||||
|
request.POST, instance=existing_status
|
||||||
|
)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
|
@ -77,7 +110,7 @@ class CreateStatus(View):
|
||||||
if hasattr(status, "quote"):
|
if hasattr(status, "quote"):
|
||||||
status.quote = to_markdown(status.quote)
|
status.quote = to_markdown(status.quote)
|
||||||
|
|
||||||
status.save(created=True)
|
status.save(created=created)
|
||||||
|
|
||||||
# update a readthorugh, if needed
|
# update a readthorugh, if needed
|
||||||
try:
|
try:
|
||||||
|
@ -106,36 +139,6 @@ class DeleteStatus(View):
|
||||||
return redirect(request.headers.get("Referer", "/"))
|
return redirect(request.headers.get("Referer", "/"))
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name="dispatch")
|
|
||||||
class DeleteAndRedraft(View):
|
|
||||||
"""delete a status but let the user re-create it"""
|
|
||||||
|
|
||||||
def post(self, request, status_id):
|
|
||||||
"""delete and tombstone a status"""
|
|
||||||
status = get_object_or_404(
|
|
||||||
models.Status.objects.select_subclasses(), id=status_id
|
|
||||||
)
|
|
||||||
# don't let people redraft other people's statuses
|
|
||||||
status.raise_not_editable(request.user)
|
|
||||||
|
|
||||||
status_type = status.status_type.lower()
|
|
||||||
if status.reply_parent:
|
|
||||||
status_type = "reply"
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"draft": status,
|
|
||||||
"type": status_type,
|
|
||||||
}
|
|
||||||
if hasattr(status, "book"):
|
|
||||||
data["book"] = status.book
|
|
||||||
elif status.mention_books:
|
|
||||||
data["book"] = status.mention_books.first()
|
|
||||||
|
|
||||||
# perform deletion
|
|
||||||
status.delete()
|
|
||||||
return TemplateResponse(request, "compose.html", data)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def update_progress(request, book_id): # pylint: disable=unused-argument
|
def update_progress(request, book_id): # pylint: disable=unused-argument
|
||||||
|
|
Loading…
Reference in a new issue