Merge branch 'main' into bookwyrm-groups
There are database migrations in main ahead of this branch so they need to be merged in to the branch before we can merge back into main.
This commit is contained in:
@ -35,6 +35,7 @@ class Note(ActivityObject):
|
||||
tag: List[Link] = field(default_factory=lambda: [])
|
||||
attachment: List[Document] = field(default_factory=lambda: [])
|
||||
sensitive: bool = False
|
||||
updated: str = None
|
||||
type: str = "Note"
|
||||
|
||||
|
||||
|
@ -69,8 +69,9 @@ class Update(Verb):
|
||||
|
||||
def action(self):
|
||||
"""update a model instance from the dataclass"""
|
||||
if self.object:
|
||||
self.object.to_model(allow_create=False)
|
||||
if not self.object:
|
||||
return
|
||||
self.object.to_model(allow_create=False)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
|
@ -3,10 +3,10 @@ from . import Importer
|
||||
|
||||
|
||||
class GoodreadsImporter(Importer):
|
||||
"""GoodReads is the default importer, thus Importer follows its structure.
|
||||
"""Goodreads is the default importer, thus Importer follows its structure.
|
||||
For a more complete example of overriding see librarything_import.py"""
|
||||
|
||||
service = "GoodReads"
|
||||
service = "Goodreads"
|
||||
|
||||
def parse_fields(self, entry):
|
||||
"""handle the specific fields in goodreads csvs"""
|
||||
|
@ -1,4 +1,4 @@
|
||||
""" handle reading a csv from an external service, defaults are from GoodReads """
|
||||
""" handle reading a csv from an external service, defaults are from Goodreads """
|
||||
import csv
|
||||
import logging
|
||||
|
||||
|
31
bookwyrm/migrations/0107_alter_user_preferred_language.py
Normal file
31
bookwyrm/migrations/0107_alter_user_preferred_language.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.2.5 on 2021-10-11 16:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0106_user_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es", "Español (Spanish)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("pt-br", "Português - Brasil (Brazilian Portugues)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
31
bookwyrm/migrations/0108_alter_user_preferred_language.py
Normal file
31
bookwyrm/migrations/0108_alter_user_preferred_language.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.2.5 on 2021-10-11 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0107_alter_user_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es-es", "Español (Spanish)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("pt-br", "Português - Brasil (Brazilian Portuguese)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
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),
|
||||
),
|
||||
]
|
23
bookwyrm/migrations/0110_auto_20211015_1734.py
Normal file
23
bookwyrm/migrations/0110_auto_20211015_1734.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.5 on 2021-10-15 17:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0109_status_edited_date"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="quotation",
|
||||
name="raw_quote",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="status",
|
||||
name="raw_content",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -67,16 +67,15 @@ class BookWyrmModel(models.Model):
|
||||
return
|
||||
|
||||
# you can see the followers only posts of people you follow
|
||||
if (
|
||||
self.privacy == "followers"
|
||||
and self.user.followers.filter(id=viewer.id).first()
|
||||
if self.privacy == "followers" and (
|
||||
self.user.followers.filter(id=viewer.id).first()
|
||||
):
|
||||
return
|
||||
|
||||
# you can see dms you are tagged in
|
||||
if hasattr(self, "mention_users"):
|
||||
if (
|
||||
self.privacy == "direct"
|
||||
self.privacy in ["direct", "followers"]
|
||||
and self.mention_users.filter(id=viewer.id).first()
|
||||
):
|
||||
|
||||
|
@ -31,6 +31,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
"User", on_delete=models.PROTECT, activitypub_field="attributedTo"
|
||||
)
|
||||
content = fields.HtmlField(blank=True, null=True)
|
||||
raw_content = models.TextField(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)
|
||||
@ -43,6 +44,9 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
published_date = fields.DateTimeField(
|
||||
default=timezone.now, activitypub_field="published"
|
||||
)
|
||||
edited_date = fields.DateTimeField(
|
||||
blank=True, null=True, activitypub_field="updated"
|
||||
)
|
||||
deleted = models.BooleanField(default=False)
|
||||
deleted_date = models.DateTimeField(blank=True, null=True)
|
||||
favorites = models.ManyToManyField(
|
||||
@ -220,6 +224,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy="direct"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def followers_filter(cls, queryset, viewer):
|
||||
"""Override-able filter for "followers" privacy level"""
|
||||
return queryset.exclude(
|
||||
~Q( # not yourself, a follower, or someone who is tagged
|
||||
Q(user__followers=viewer) | Q(user=viewer) | Q(mention_users=viewer)
|
||||
),
|
||||
privacy="followers", # and the status is followers only
|
||||
)
|
||||
|
||||
|
||||
class GeneratedNote(Status):
|
||||
"""these are app-generated messages about user activity"""
|
||||
@ -292,6 +306,7 @@ class Quotation(BookStatus):
|
||||
"""like a review but without a rating and transient"""
|
||||
|
||||
quote = fields.HtmlField()
|
||||
raw_quote = models.TextField(blank=True, null=True)
|
||||
position = models.IntegerField(
|
||||
validators=[MinValueValidator(0)], null=True, blank=True
|
||||
)
|
||||
|
@ -7,13 +7,14 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.0.1"
|
||||
VERSION = "0.1.0"
|
||||
|
||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "c02929b1"
|
||||
JS_CACHE = "3eb4edb1"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
@ -162,11 +163,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
LANGUAGE_CODE = "en-us"
|
||||
LANGUAGES = [
|
||||
("en-us", _("English")),
|
||||
("de-de", _("Deutsch (German)")), # German
|
||||
("es", _("Español (Spanish)")), # Spanish
|
||||
("fr-fr", _("Français (French)")), # French
|
||||
("zh-hans", _("简体中文 (Simplified Chinese)")), # Simplified Chinese
|
||||
("zh-hant", _("繁體中文 (Traditional Chinese)")), # Traditional Chinese
|
||||
("de-de", _("Deutsch (German)")),
|
||||
("es-es", _("Español (Spanish)")),
|
||||
("fr-fr", _("Français (French)")),
|
||||
("pt-br", _("Português - Brasil (Brazilian Portuguese)")),
|
||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||
]
|
||||
|
||||
|
||||
|
@ -509,6 +509,20 @@ ol.ordered-list li::before {
|
||||
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
|
||||
* @todo These could be in rem.
|
||||
******************************************************************************/
|
||||
|
@ -12,7 +12,9 @@
|
||||
<div>
|
||||
<p>{% trans "Added:" %} {{ author.created_date | naturaltime }}</p>
|
||||
<p>{% trans "Updated:" %} {{ author.updated_date | naturaltime }}</p>
|
||||
{% if author.last_edited_by %}
|
||||
<p>{% trans "Last edited by:" %} <a href="{{ author.last_edited_by.remote_id }}">{{ author.last_edited_by.display_name }}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% spaceless %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if book.isbn13 or book.oclc_number or book.asin %}
|
||||
{% if book.isbn_13 or book.oclc_number or book.asin %}
|
||||
<dl>
|
||||
{% if book.isbn_13 %}
|
||||
<div class="is-flex">
|
||||
|
@ -108,7 +108,13 @@
|
||||
{% if not confirm_mode %}
|
||||
<div class="block">
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
{% if book %}
|
||||
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
||||
{% else %}
|
||||
<a href="/" class="button" data-back>
|
||||
<span>{% trans "Cancel" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% firstof book.physical_format_detail book.physical_format as format %}
|
||||
{% firstof book.physical_format_detail book.get_physical_format_display as format %}
|
||||
{% firstof book.physical_format book.physical_format_detail as format_property %}
|
||||
{% with pages=book.pages %}
|
||||
{% if format or pages %}
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
<p>
|
||||
{% if format and not pages %}
|
||||
{% blocktrans %}{{ format }}{% endblocktrans %}
|
||||
{{ format }}
|
||||
{% elif format and pages %}
|
||||
{% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %}
|
||||
{% elif pages %}
|
||||
|
@ -2,10 +2,10 @@
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{% trans "Compose status" %}{% endblock %}
|
||||
{% block title %}{% trans "Edit status" %}{% endblock %}
|
||||
{% block content %}
|
||||
<header class="block content">
|
||||
<h1>{% trans "Compose status" %}</h1>
|
||||
<h1>{% trans "Edit status" %}</h1>
|
||||
</header>
|
||||
|
||||
{% with 0|uuid as uuid %}
|
||||
@ -22,6 +22,10 @@
|
||||
<div class="column">
|
||||
{% if draft.reply_parent %}
|
||||
{% include 'snippets/status/status.html' with status=draft.reply_parent no_interact=True %}
|
||||
{% else %}
|
||||
<div class="block">
|
||||
{% include "snippets/status/header.html" with status=draft %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not draft %}
|
||||
|
26
bookwyrm/templates/discover/card-header.html
Normal file
26
bookwyrm/templates/discover/card-header.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% with user_path=status.user.local_path username=status.user.display_name book_path=status.book.local_poth book_title=book|book_title %}
|
||||
|
||||
{% if status.status_type == 'GeneratedNote' %}
|
||||
{{ status.content|safe }}
|
||||
{% elif status.status_type == 'Rating' %}
|
||||
{% blocktrans trimmed %}
|
||||
<a href="{{ user_path}}">{{ username }}</a> rated <a href="{{ book_path }}">{{ book_title }}</a>
|
||||
{% endblocktrans %}
|
||||
{% elif status.status_type == 'Review' %}
|
||||
{% blocktrans trimmed %}
|
||||
<a href="{{ user_path}}">{{ username }}</a> reviewed <a href="{{ book_path }}">{{ book_title }}</a>
|
||||
{% endblocktrans %}
|
||||
{% elif status.status_type == 'Comment' %}
|
||||
{% blocktrans trimmed %}
|
||||
<a href="{{ user_path}}">{{ username }}</a> commented on <a href="{{ book_path }}">{{ book_title }}</a>
|
||||
{% endblocktrans %}
|
||||
{% elif status.status_type == 'Quotation' %}
|
||||
{% blocktrans trimmed %}
|
||||
<a href="{{ user_path}}">{{ username }}</a> quoted <a href="{{ book_path }}">{{ book_title }}</a>
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
@ -36,23 +36,7 @@
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<h3 class="title is-6">
|
||||
<a href="{{ status.user.local_path }}">
|
||||
<span>{{ status.user.display_name }}</span>
|
||||
</a>
|
||||
|
||||
{% if status.status_type == 'GeneratedNote' %}
|
||||
{{ status.content|safe }}
|
||||
{% elif status.status_type == 'Rating' %}
|
||||
{% trans "rated" %}
|
||||
{% elif status.status_type == 'Review' %}
|
||||
{% trans "reviewed" %}
|
||||
{% elif status.status_type == 'Comment' %}
|
||||
{% trans "commented on" %}
|
||||
{% elif status.status_type == 'Quotation' %}
|
||||
{% trans "quoted" %}
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ book.local_path }}">{{ book.title }}</a>
|
||||
{% include "discover/card-header.html" %}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,23 +22,7 @@
|
||||
|
||||
<div class="media-content">
|
||||
<h3 class="title is-6">
|
||||
<a href="{{ status.user.local_path }}">
|
||||
<span>{{ status.user.display_name }}</span>
|
||||
</a>
|
||||
|
||||
{% if status.status_type == 'GeneratedNote' %}
|
||||
{{ status.content|safe }}
|
||||
{% elif status.status_type == 'Rating' %}
|
||||
{% trans "rated" %}
|
||||
{% elif status.status_type == 'Review' %}
|
||||
{% trans "reviewed" %}
|
||||
{% elif status.status_type == 'Comment' %}
|
||||
{% trans "commented on" %}
|
||||
{% elif status.status_type == 'Quotation' %}
|
||||
{% trans "quoted" %}
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ book.local_path }}">{{ book.title }}</a>
|
||||
{% include "discover/card-header.html" %}
|
||||
</h3>
|
||||
{% if status.rating %}
|
||||
<p class="subtitle is-6">
|
||||
|
@ -12,6 +12,6 @@
|
||||
<p>
|
||||
{% url 'code-of-conduct' as coc_path %}
|
||||
{% url 'about' as about_path %}
|
||||
{% blocktrans %}Learn more <a href="https://{{ domain }}{{ about_path }}">about this instance</a>.{% endblocktrans %}
|
||||
{% blocktrans %}Learn more <a href="https://{{ domain }}{{ about_path }}">about {{ site_name }}</a>.{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
@ -5,6 +5,6 @@
|
||||
|
||||
{{ invite_link }}
|
||||
|
||||
{% trans "Learn more about this instance:" %} https://{{ domain }}{% url 'about' %}
|
||||
{% blocktrans %}Learn more about {{ site_name }}:{% endblocktrans %} https://{{ domain }}{% url 'about' %}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -22,8 +22,8 @@
|
||||
|
||||
<div class="select block">
|
||||
<select name="source" id="source">
|
||||
<option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}>
|
||||
GoodReads (CSV)
|
||||
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
|
||||
Goodreads (CSV)
|
||||
</option>
|
||||
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
|
||||
Storygraph (CSV)
|
||||
|
@ -3,6 +3,6 @@
|
||||
|
||||
{% block tooltip_content %}
|
||||
|
||||
{% trans 'You can download your GoodReads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener">Import/Export page</a> of your GoodReads account.' %}
|
||||
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener">Import/Export page</a> of your Goodreads account.' %}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -227,7 +227,7 @@
|
||||
<div class="columns">
|
||||
<div class="column is-one-fifth">
|
||||
<p>
|
||||
<a href="{% url 'about' %}">{% trans "About this instance" %}</a>
|
||||
<a href="{% url 'about' %}">{% blocktrans with site_name=site.name %}About {{ site_name }}{% endblocktrans %}</a>
|
||||
</p>
|
||||
{% if site.admin_email %}
|
||||
<p>
|
||||
|
@ -18,25 +18,25 @@
|
||||
{% if related_status.status_type == 'Review' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
favorited your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
|
||||
liked your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Comment' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
favorited your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>
|
||||
liked your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% elif related_status.status_type == 'Quotation' %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
favorited your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
|
||||
liked your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
favorited your <a href="{{ related_path }}">status</a>
|
||||
liked your <a href="{{ related_path }}">status</a>
|
||||
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label>
|
||||
<p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." %}</p>
|
||||
<p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support HTML or Markdown." %}</p>
|
||||
{{ site_form.instance_short_description }}
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -124,14 +124,16 @@
|
||||
<table class="table is-striped is-fullwidth is-mobile">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Cover" %}</th>
|
||||
<th>{% trans "Title" %}</th>
|
||||
<th>{% trans "Author" %}</th>
|
||||
<th>{% trans "Shelved" %}</th>
|
||||
<th>{% trans "Started" %}</th>
|
||||
<th>{% trans "Finished" %}</th>
|
||||
<th>{% trans "Cover"%}</th>
|
||||
<th>{% trans "Title" as text %}{% include 'snippets/table-sort-header.html' with field="title" sort=sort text=text %}</th>
|
||||
<th>{% trans "Author" as text %}{% include 'snippets/table-sort-header.html' with field="author" sort=sort text=text %}</th>
|
||||
{% if request.user.is_authenticated %}
|
||||
<th>{% trans "Rating" %}</th>
|
||||
{% if is_self %}
|
||||
<th>{% trans "Shelved" as text %}{% include 'snippets/table-sort-header.html' with field="shelved_date" sort=sort text=text %}</th>
|
||||
<th>{% trans "Started" as text %}{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}</th>
|
||||
<th>{% trans "Finished" as text %}{% include 'snippets/table-sort-header.html' with field="finish_date" sort=sort text=text %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Rating" as text %}{% include 'snippets/table-sort-header.html' with field="rating" sort=sort text=text %}</th>
|
||||
{% endif %}
|
||||
{% if shelf.user == request.user %}
|
||||
<th aria-hidden="true"></th>
|
||||
@ -151,17 +153,18 @@
|
||||
<td data-title="{% trans "Author" %}">
|
||||
{% include 'snippets/authors.html' %}
|
||||
</td>
|
||||
{% if request.user.is_authenticated %}
|
||||
{% if is_self %}
|
||||
<td data-title="{% trans "Shelved" %}">
|
||||
{{ book.shelved_date|naturalday }}
|
||||
</td>
|
||||
{% latest_read_through book user as read_through %}
|
||||
<td data-title="{% trans "Started" %}">
|
||||
{{ read_through.start_date|naturalday|default_if_none:""}}
|
||||
{{ book.start_date|naturalday|default_if_none:""}}
|
||||
</td>
|
||||
<td data-title="{% trans "Finished" %}">
|
||||
{{ read_through.finish_date|naturalday|default_if_none:""}}
|
||||
{{ book.finish_date|naturalday|default_if_none:""}}
|
||||
</td>
|
||||
{% if request.user.is_authenticated %}
|
||||
{% endif %}
|
||||
<td data-title="{% trans "Rating" %}">
|
||||
{% include 'snippets/stars.html' with rating=book.rating %}
|
||||
</td>
|
||||
|
@ -15,6 +15,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||
{% trans "Some thoughts on the book" as placeholder %}
|
||||
|
||||
{% block post_content_additions %}
|
||||
{% if not draft.reading_status %}
|
||||
{# Supplemental fields #}
|
||||
<div>
|
||||
{% active_shelf book as active_shelf %}
|
||||
@ -35,7 +36,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||
size="3"
|
||||
value="{% firstof draft.progress readthrough.progress '' %}"
|
||||
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 class="control">
|
||||
@ -43,7 +44,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||
<select
|
||||
name="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
|
||||
value="PG"
|
||||
@ -68,6 +69,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% endwith %}
|
||||
|
@ -11,10 +11,10 @@ draft: an existing Status object that is providing default values for input fiel
|
||||
<textarea
|
||||
name="content"
|
||||
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 }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
|
||||
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}
|
||||
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{{ draft.content|default:'' }}</textarea>
|
||||
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{% firstof draft.raw_content draft.content '' %}</textarea>
|
||||
|
||||
|
@ -17,6 +17,6 @@
|
||||
id="id_content_warning_{{ uuid }}{{ local_uuid }}"
|
||||
placeholder="{% trans 'Spoilers ahead!' %}"
|
||||
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>
|
||||
|
@ -8,7 +8,7 @@
|
||||
id="id_show_spoilers_{{ uuid }}{{ local_uuid }}"
|
||||
{% if draft.content_warning or status.content_warning %}checked{% endif %}
|
||||
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 %}
|
||||
{% 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
|
||||
class="is-flex-grow-1{% if not no_script %} submit-status{% endif %}"
|
||||
name="{{ type }}"
|
||||
action="/post/{{ type }}"
|
||||
{% if draft %}
|
||||
action="{% url 'create-status' type draft.id %}"
|
||||
{% else %}
|
||||
action="{% url 'create-status' type %}"
|
||||
{% endif %}
|
||||
method="post"
|
||||
id="form_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
||||
>
|
||||
@ -29,6 +33,9 @@ reply_parent: the Status object this post will be in reply to, if applicable
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
<input type="hidden" name="reply_parent" value="{% firstof draft.reply_parent.id reply_parent.id %}">
|
||||
{% if draft %}
|
||||
<input type="hidden" name="reading_status" value="{{ draft.reading_status|default:'' }}">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% include "snippets/create_status/content_warning_field.html" %}
|
||||
|
@ -24,8 +24,8 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||
id="id_quote_{{ book.id }}_{{ type }}"
|
||||
placeholder="{% blocktrans with book_title=book.title %}An excerpt from '{{ book_title }}'{% endblocktrans %}"
|
||||
required
|
||||
data-cache-draft="id_quote_{{ book.id }}_{{ type }}"
|
||||
>{{ draft.quote|default:'' }}</textarea>
|
||||
{% if not draft %}data-cache-draft="id_quote_{{ book.id }}_{{ type }}"{% endif %}
|
||||
>{% firstof draft.raw_quote draft.quote '' %}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
@ -36,7 +36,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||
<select
|
||||
name="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
|
||||
value="PG"
|
||||
@ -63,7 +63,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||
size="3"
|
||||
value="{% firstof draft.position '' %}"
|
||||
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>
|
||||
|
@ -24,7 +24,7 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||
id="id_name_{{ book.id }}"
|
||||
placeholder="{% blocktrans with book_title=book.title %}Your review of '{{ book_title }}'{% endblocktrans %}"
|
||||
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>
|
||||
|
@ -1,7 +1,19 @@
|
||||
{% spaceless %}
|
||||
|
||||
{% load humanize %}
|
||||
{% load i18n %}
|
||||
|
||||
{% 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 %}
|
||||
{% blocktrans with page=page|intcomma %}page {{ page }}{% endblocktrans %}
|
||||
|
||||
{% blocktrans trimmed with page=page|intcomma %}
|
||||
page {{ page }}
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% if request.user.is_authenticated %}
|
||||
<span class="is-sr-only">{% trans "Leave a rating" %}</span>
|
||||
<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 %}
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
|
@ -31,18 +31,38 @@
|
||||
|
||||
{% include "snippets/status/header_content.html" %}
|
||||
</h3>
|
||||
<p class="is-size-7 is-flex is-align-items-center">
|
||||
<a href="{{ status.remote_id }}{% if status.user.local %}#anchor-{{ status.id }}{% endif %}">{{ status.published_date|published_date }}</a>
|
||||
{% if status.progress %}
|
||||
<span class="ml-1">
|
||||
{% if status.progress_mode == 'PG' %}
|
||||
({% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %})
|
||||
{% else %}
|
||||
({{ status.progress }}%)
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% include 'snippets/privacy-icons.html' with item=status %}
|
||||
</p>
|
||||
<div class="breadcrumb has-dot-separator is-small">
|
||||
<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 %}
|
||||
<li class="ml-1">
|
||||
<span>
|
||||
{% if status.progress_mode == 'PG' %}
|
||||
{% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %}
|
||||
{% else %}
|
||||
{{ status.progress }}%
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
{% include 'snippets/privacy-icons.html' with item=status %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,7 +67,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
<section class="reply-panel is-hidden" id="show_comment_{{ status.id }}">
|
||||
<div class="card-footer">
|
||||
|
@ -20,12 +20,9 @@
|
||||
</li>
|
||||
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
|
||||
<li role="menuitem" class="dropdown-item p-0">
|
||||
<form class="" name="delete-{{ status.id }}" action="{% url 'redraft' status.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
|
||||
{% trans "Delete & re-draft" %}
|
||||
</button>
|
||||
</form>
|
||||
<a href="{% url 'edit-status' status.id %}" class="button is-radiusless is-fullwidth is-small" type="submit">
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
@ -3,11 +3,13 @@ from unittest.mock import patch
|
||||
from io import BytesIO
|
||||
import pathlib
|
||||
|
||||
from PIL import Image
|
||||
from django.http import Http404
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import IntegrityError
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from PIL import Image
|
||||
import responses
|
||||
|
||||
from bookwyrm import activitypub, models, settings
|
||||
@ -50,6 +52,9 @@ class Status(TestCase):
|
||||
image.save(output, format=image.format)
|
||||
self.book.cover.save("test.jpg", ContentFile(output.getvalue()))
|
||||
|
||||
self.anonymous_user = AnonymousUser
|
||||
self.anonymous_user.is_authenticated = False
|
||||
|
||||
def test_status_generated_fields(self, *_):
|
||||
"""setting remote id"""
|
||||
status = models.Status.objects.create(content="bleh", user=self.local_user)
|
||||
@ -460,3 +465,60 @@ class Status(TestCase):
|
||||
responses.add(responses.GET, "http://fish.com/nothing", status=404)
|
||||
|
||||
self.assertTrue(models.Status.ignore_activity(activity))
|
||||
|
||||
def test_raise_visible_to_user_public(self, *_):
|
||||
"""privacy settings"""
|
||||
status = models.Status.objects.create(
|
||||
content="bleh", user=self.local_user, privacy="public"
|
||||
)
|
||||
self.assertIsNone(status.raise_visible_to_user(self.remote_user))
|
||||
self.assertIsNone(status.raise_visible_to_user(self.local_user))
|
||||
self.assertIsNone(status.raise_visible_to_user(self.anonymous_user))
|
||||
|
||||
def test_raise_visible_to_user_unlisted(self, *_):
|
||||
"""privacy settings"""
|
||||
status = models.Status.objects.create(
|
||||
content="bleh", user=self.local_user, privacy="unlisted"
|
||||
)
|
||||
self.assertIsNone(status.raise_visible_to_user(self.remote_user))
|
||||
self.assertIsNone(status.raise_visible_to_user(self.local_user))
|
||||
self.assertIsNone(status.raise_visible_to_user(self.anonymous_user))
|
||||
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
def test_raise_visible_to_user_followers(self, *_):
|
||||
"""privacy settings"""
|
||||
status = models.Status.objects.create(
|
||||
content="bleh", user=self.local_user, privacy="followers"
|
||||
)
|
||||
status.raise_visible_to_user(self.local_user)
|
||||
with self.assertRaises(Http404):
|
||||
status.raise_visible_to_user(self.remote_user)
|
||||
with self.assertRaises(Http404):
|
||||
status.raise_visible_to_user(self.anonymous_user)
|
||||
|
||||
self.local_user.followers.add(self.remote_user)
|
||||
self.assertIsNone(status.raise_visible_to_user(self.remote_user))
|
||||
|
||||
def test_raise_visible_to_user_followers_mentioned(self, *_):
|
||||
"""privacy settings"""
|
||||
status = models.Status.objects.create(
|
||||
content="bleh", user=self.local_user, privacy="followers"
|
||||
)
|
||||
status.mention_users.set([self.remote_user])
|
||||
self.assertIsNone(status.raise_visible_to_user(self.remote_user))
|
||||
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
def test_raise_visible_to_user_direct(self, *_):
|
||||
"""privacy settings"""
|
||||
status = models.Status.objects.create(
|
||||
content="bleh", user=self.local_user, privacy="direct"
|
||||
)
|
||||
status.raise_visible_to_user(self.local_user)
|
||||
with self.assertRaises(Http404):
|
||||
status.raise_visible_to_user(self.remote_user)
|
||||
with self.assertRaises(Http404):
|
||||
status.raise_visible_to_user(self.anonymous_user)
|
||||
|
||||
# mentioned user
|
||||
status.mention_users.set([self.remote_user])
|
||||
self.assertIsNone(status.raise_visible_to_user(self.remote_user))
|
||||
|
@ -58,6 +58,17 @@ class EditBookViews(TestCase):
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_edit_book_create_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.EditBook.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_edit_book(self):
|
||||
"""lets a user edit a book"""
|
||||
view = views.EditBook.as_view()
|
||||
|
@ -37,9 +37,9 @@ class InboxUpdate(TestCase):
|
||||
outbox="https://example.com/users/rat/outbox",
|
||||
)
|
||||
|
||||
self.create_json = {
|
||||
self.update_json = {
|
||||
"id": "hi",
|
||||
"type": "Create",
|
||||
"type": "Update",
|
||||
"actor": "hi",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||
"cc": ["https://example.com/user/mouse/followers"],
|
||||
@ -54,26 +54,20 @@ class InboxUpdate(TestCase):
|
||||
book_list = models.List.objects.create(
|
||||
name="hi", remote_id="https://example.com/list/22", user=self.local_user
|
||||
)
|
||||
activity = {
|
||||
"type": "Update",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"actor": "hi",
|
||||
"id": "sdkjf",
|
||||
"object": {
|
||||
"id": "https://example.com/list/22",
|
||||
"type": "BookList",
|
||||
"totalItems": 1,
|
||||
"first": "https://example.com/list/22?page=1",
|
||||
"last": "https://example.com/list/22?page=1",
|
||||
"name": "Test List",
|
||||
"owner": "https://example.com/user/mouse",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc": ["https://example.com/user/mouse/followers"],
|
||||
"summary": "summary text",
|
||||
"curation": "curated",
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
},
|
||||
activity = self.update_json
|
||||
activity["object"] = {
|
||||
"id": "https://example.com/list/22",
|
||||
"type": "BookList",
|
||||
"totalItems": 1,
|
||||
"first": "https://example.com/list/22?page=1",
|
||||
"last": "https://example.com/list/22?page=1",
|
||||
"name": "Test List",
|
||||
"owner": "https://example.com/user/mouse",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc": ["https://example.com/user/mouse/followers"],
|
||||
"summary": "summary text",
|
||||
"curation": "curated",
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
}
|
||||
views.inbox.activity_task(activity)
|
||||
book_list.refresh_from_db()
|
||||
@ -176,3 +170,26 @@ class InboxUpdate(TestCase):
|
||||
)
|
||||
book = models.Work.objects.get(id=book.id)
|
||||
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)
|
||||
|
@ -9,6 +9,7 @@ from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class AuthorViews(TestCase):
|
||||
@ -54,7 +55,7 @@ class AuthorViews(TestCase):
|
||||
is_api.return_value = False
|
||||
result = view(request, author.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
@ -75,7 +76,7 @@ class AuthorViews(TestCase):
|
||||
|
||||
result = view(request, author.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
|
@ -7,6 +7,7 @@ from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@ -50,7 +51,7 @@ class StatusViews(TestCase):
|
||||
)
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_handle_status(self, *_):
|
||||
def test_create_status_comment(self, *_):
|
||||
"""create a status"""
|
||||
view = views.CreateStatus.as_view()
|
||||
form = forms.CommentForm(
|
||||
@ -67,11 +68,13 @@ class StatusViews(TestCase):
|
||||
view(request, "comment")
|
||||
|
||||
status = models.Comment.objects.get()
|
||||
self.assertEqual(status.raw_content, "hi")
|
||||
self.assertEqual(status.content, "<p>hi</p>")
|
||||
self.assertEqual(status.user, self.local_user)
|
||||
self.assertEqual(status.book, self.book)
|
||||
self.assertIsNone(status.edited_date)
|
||||
|
||||
def test_handle_status_reply(self, *_):
|
||||
def test_create_status_reply(self, *_):
|
||||
"""create a status in reply to an existing status"""
|
||||
view = views.CreateStatus.as_view()
|
||||
user = models.User.objects.create_user(
|
||||
@ -98,7 +101,7 @@ class StatusViews(TestCase):
|
||||
self.assertEqual(status.user, user)
|
||||
self.assertEqual(models.Notification.objects.get().user, self.local_user)
|
||||
|
||||
def test_handle_status_mentions(self, *_):
|
||||
def test_create_status_mentions(self, *_):
|
||||
"""@mention a user in a post"""
|
||||
view = views.CreateStatus.as_view()
|
||||
user = models.User.objects.create_user(
|
||||
@ -128,7 +131,7 @@ class StatusViews(TestCase):
|
||||
status.content, f'<p>hi <a href="{user.remote_id}">@rat</a></p>'
|
||||
)
|
||||
|
||||
def test_handle_status_reply_with_mentions(self, *_):
|
||||
def test_create_status_reply_with_mentions(self, *_):
|
||||
"""reply to a post with an @mention'ed user"""
|
||||
view = views.CreateStatus.as_view()
|
||||
user = models.User.objects.create_user(
|
||||
@ -168,60 +171,6 @@ class StatusViews(TestCase):
|
||||
self.assertFalse(self.remote_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, *_):
|
||||
"""detect and look up @ mentions of users"""
|
||||
user = models.User.objects.create_user(
|
||||
@ -349,7 +298,7 @@ http://www.fish.com/"""
|
||||
result = views.status.to_markdown(text)
|
||||
self.assertEqual(result, '<p><a href="http://fish.com">hi</a> ' "is rad</p>")
|
||||
|
||||
def test_handle_delete_status(self, mock, *_):
|
||||
def test_delete_status(self, mock, *_):
|
||||
"""marks a status as deleted"""
|
||||
view = views.DeleteStatus.as_view()
|
||||
with patch("bookwyrm.activitystreams.add_status_task.delay"):
|
||||
@ -367,7 +316,7 @@ http://www.fish.com/"""
|
||||
status.refresh_from_db()
|
||||
self.assertTrue(status.deleted)
|
||||
|
||||
def test_handle_delete_status_permission_denied(self, *_):
|
||||
def test_delete_status_permission_denied(self, *_):
|
||||
"""marks a status as deleted"""
|
||||
view = views.DeleteStatus.as_view()
|
||||
with patch("bookwyrm.activitystreams.add_status_task.delay"):
|
||||
@ -382,7 +331,7 @@ http://www.fish.com/"""
|
||||
status.refresh_from_db()
|
||||
self.assertFalse(status.deleted)
|
||||
|
||||
def test_handle_delete_status_moderator(self, mock, *_):
|
||||
def test_delete_status_moderator(self, mock, *_):
|
||||
"""marks a status as deleted"""
|
||||
view = views.DeleteStatus.as_view()
|
||||
with patch("bookwyrm.activitystreams.add_status_task.delay"):
|
||||
@ -400,3 +349,75 @@ http://www.fish.com/"""
|
||||
self.assertEqual(activity["object"]["type"], "Tombstone")
|
||||
status.refresh_from_db()
|
||||
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_edit_status_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_edit_status_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)
|
||||
|
@ -342,6 +342,9 @@ urlpatterns = [
|
||||
re_path(
|
||||
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(
|
||||
r"^post/?$",
|
||||
views.CreateStatus.as_view(),
|
||||
@ -352,16 +355,16 @@ urlpatterns = [
|
||||
views.CreateStatus.as_view(),
|
||||
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(
|
||||
r"^delete-status/(?P<status_id>\d+)/?$",
|
||||
views.DeleteStatus.as_view(),
|
||||
name="delete-status",
|
||||
),
|
||||
re_path(
|
||||
r"^redraft-status/(?P<status_id>\d+)/?$",
|
||||
views.DeleteAndRedraft.as_view(),
|
||||
name="redraft",
|
||||
),
|
||||
# interact
|
||||
re_path(r"^favorite/(?P<status_id>\d+)/?$", views.Favorite.as_view(), name="fav"),
|
||||
re_path(
|
||||
|
@ -70,7 +70,7 @@ from .search import Search
|
||||
from .shelf import Shelf
|
||||
from .shelf import create_shelf, delete_shelf
|
||||
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 .updates import get_notification_count, get_unread_status_count
|
||||
from .user import User, Followers, Following, hide_suggestions
|
||||
|
@ -51,7 +51,7 @@ class Import(View):
|
||||
elif source == "Storygraph":
|
||||
importer = StorygraphImporter()
|
||||
else:
|
||||
# Default : GoodReads
|
||||
# Default : Goodreads
|
||||
importer = GoodreadsImporter()
|
||||
|
||||
try:
|
||||
|
@ -68,16 +68,30 @@ class Shelf(View):
|
||||
deleted=False,
|
||||
).order_by("-published_date")
|
||||
|
||||
reading = models.ReadThrough.objects
|
||||
|
||||
reading = reading.filter(user=user, book__id=OuterRef("id")).order_by(
|
||||
"start_date"
|
||||
)
|
||||
|
||||
books = books.annotate(
|
||||
rating=Subquery(reviews.values("rating")[:1]),
|
||||
shelved_date=F("shelfbook__shelved_date"),
|
||||
start_date=Subquery(reading.values("start_date")[:1]),
|
||||
finish_date=Subquery(reading.values("finish_date")[:1]),
|
||||
author=Subquery(
|
||||
models.Book.objects.filter(id=OuterRef("id")).values("authors__name")[
|
||||
:1
|
||||
]
|
||||
),
|
||||
).prefetch_related("authors")
|
||||
|
||||
books = sort_books(books, request.GET.get("sort"))
|
||||
|
||||
paginated = Paginator(
|
||||
books.order_by("-shelfbook__updated_date"),
|
||||
books,
|
||||
PAGE_LENGTH,
|
||||
)
|
||||
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"user": user,
|
||||
@ -87,6 +101,7 @@ class Shelf(View):
|
||||
"books": page,
|
||||
"edit_form": forms.ShelfForm(instance=shelf if shelf_identifier else None),
|
||||
"create_form": forms.ShelfForm(),
|
||||
"sort": request.GET.get("sort"),
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
@ -207,3 +222,23 @@ def unshelve(request):
|
||||
|
||||
shelf_book.delete()
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
||||
|
||||
def sort_books(books, sort):
|
||||
"""Books in shelf sorting"""
|
||||
sort_fields = [
|
||||
"title",
|
||||
"author",
|
||||
"shelved_date",
|
||||
"start_date",
|
||||
"finish_date",
|
||||
"rating",
|
||||
]
|
||||
|
||||
if sort in sort_fields:
|
||||
books = books.order_by(sort)
|
||||
elif sort and sort[1:] in sort_fields:
|
||||
books = books.order_by(F(sort[1:]).desc(nulls_last=True))
|
||||
else:
|
||||
books = books.order_by("-shelved_date")
|
||||
return books
|
||||
|
@ -8,6 +8,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
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
|
||||
|
||||
|
||||
# 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
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class CreateStatus(View):
|
||||
"""the view for *posting*"""
|
||||
|
||||
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"))
|
||||
data = {"book": book}
|
||||
return TemplateResponse(request, "compose.html", data)
|
||||
|
||||
def post(self, request, status_type):
|
||||
"""create status of whatever type"""
|
||||
def post(self, request, status_type, existing_status_id=None):
|
||||
"""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:]
|
||||
|
||||
try:
|
||||
form = getattr(forms, f"{status_type}Form")(request.POST)
|
||||
form = getattr(forms, f"{status_type}Form")(
|
||||
request.POST, instance=existing_status
|
||||
)
|
||||
except AttributeError:
|
||||
return HttpResponseBadRequest()
|
||||
if not form.is_valid():
|
||||
@ -46,6 +79,11 @@ class CreateStatus(View):
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
||||
status = form.save(commit=False)
|
||||
# save the plain, unformatted version of the status for future editing
|
||||
status.raw_content = status.content
|
||||
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
|
||||
@ -77,7 +115,7 @@ class CreateStatus(View):
|
||||
if hasattr(status, "quote"):
|
||||
status.quote = to_markdown(status.quote)
|
||||
|
||||
status.save(created=True)
|
||||
status.save(created=created)
|
||||
|
||||
# update a readthorugh, if needed
|
||||
try:
|
||||
@ -106,36 +144,6 @@ class DeleteStatus(View):
|
||||
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
|
||||
@require_POST
|
||||
def update_progress(request, book_id): # pylint: disable=unused-argument
|
||||
|
Reference in New Issue
Block a user