Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-05-18 14:40:54 -07:00
commit 1e71cf980c
93 changed files with 3000 additions and 2562 deletions

View file

@ -30,7 +30,6 @@ class AbstractMinimalConnector(ABC):
"covers_url", "covers_url",
"search_url", "search_url",
"isbn_search_url", "isbn_search_url",
"max_query_count",
"name", "name",
"identifier", "identifier",
"local", "local",
@ -102,13 +101,6 @@ class AbstractConnector(AbstractMinimalConnector):
# title we handle separately. # title we handle separately.
self.book_mappings = [] self.book_mappings = []
def is_available(self):
"""check if you're allowed to use this connector"""
if self.max_query_count is not None:
if self.connector.query_count >= self.max_query_count:
return False
return True
def get_or_create_book(self, remote_id): def get_or_create_book(self, remote_id):
"""translate arbitrary json into an Activitypub dataclass""" """translate arbitrary json into an Activitypub dataclass"""
# first, check if we have the origin_id saved # first, check if we have the origin_id saved

View file

@ -87,7 +87,7 @@ def first_search_result(query, min_confidence=0.1):
def get_connectors(): def get_connectors():
"""load all connectors""" """load all connectors"""
for info in models.Connector.objects.order_by("priority").all(): for info in models.Connector.objects.filter(active=True).order_by("priority").all():
yield load_connector(info) yield load_connector(info)

View file

@ -0,0 +1,48 @@
# Generated by Django 3.2 on 2021-05-11 18:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0073_sitesettings_footer_item"),
]
operations = [
migrations.RemoveField(
model_name="connector",
name="max_query_count",
),
migrations.RemoveField(
model_name="connector",
name="politeness_delay",
),
migrations.RemoveField(
model_name="connector",
name="query_count",
),
migrations.RemoveField(
model_name="connector",
name="query_count_expiry",
),
migrations.AddField(
model_name="connector",
name="active",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="connector",
name="deactivation_reason",
field=models.CharField(
blank=True,
choices=[
("self_deletion", "Self Deletion"),
("moderator_deletion", "Moderator Deletion"),
("domain_block", "Domain Block"),
],
max_length=255,
null=True,
),
),
]

View file

@ -6,6 +6,16 @@ from bookwyrm.settings import DOMAIN
from .fields import RemoteIdField from .fields import RemoteIdField
DeactivationReason = models.TextChoices(
"DeactivationReason",
[
"self_deletion",
"moderator_deletion",
"domain_block",
],
)
class BookWyrmModel(models.Model): class BookWyrmModel(models.Model):
"""shared fields""" """shared fields"""

View file

@ -2,7 +2,7 @@
from django.db import models from django.db import models
from bookwyrm.connectors.settings import CONNECTORS from bookwyrm.connectors.settings import CONNECTORS
from .base_model import BookWyrmModel from .base_model import BookWyrmModel, DeactivationReason
ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS) ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
@ -17,6 +17,10 @@ class Connector(BookWyrmModel):
local = models.BooleanField(default=False) local = models.BooleanField(default=False)
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices) connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
api_key = models.CharField(max_length=255, null=True, blank=True) api_key = models.CharField(max_length=255, null=True, blank=True)
active = models.BooleanField(default=True)
deactivation_reason = models.CharField(
max_length=255, choices=DeactivationReason.choices, null=True, blank=True
)
base_url = models.CharField(max_length=255) base_url = models.CharField(max_length=255)
books_url = models.CharField(max_length=255) books_url = models.CharField(max_length=255)
@ -24,13 +28,6 @@ class Connector(BookWyrmModel):
search_url = models.CharField(max_length=255, null=True, blank=True) search_url = models.CharField(max_length=255, null=True, blank=True)
isbn_search_url = models.CharField(max_length=255, null=True, blank=True) isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
politeness_delay = models.IntegerField(null=True, blank=True) # seconds
max_query_count = models.IntegerField(null=True, blank=True)
# how many queries executed in a unit of time, like a day
query_count = models.IntegerField(default=0)
# when to reset the query count back to 0 (ie, after 1 day)
query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True)
def __str__(self): def __str__(self):
return "{} ({})".format( return "{} ({})".format(
self.identifier, self.identifier,

View file

@ -1,5 +1,6 @@
""" connections to external ActivityPub servers """ """ connections to external ActivityPub servers """
from urllib.parse import urlparse from urllib.parse import urlparse
from django.apps import apps
from django.db import models from django.db import models
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -34,6 +35,13 @@ class FederatedServer(BookWyrmModel):
is_active=False, deactivation_reason="domain_block" is_active=False, deactivation_reason="domain_block"
) )
# check for related connectors
if self.application_type == "bookwyrm":
connector_model = apps.get_model("bookwyrm.Connector", require_ready=True)
connector_model.objects.filter(
identifier=self.server_name, active=True
).update(active=False, deactivation_reason="domain_block")
def unblock(self): def unblock(self):
"""unblock a server""" """unblock a server"""
self.status = "federated" self.status = "federated"
@ -43,6 +51,15 @@ class FederatedServer(BookWyrmModel):
is_active=True, deactivation_reason=None is_active=True, deactivation_reason=None
) )
# check for related connectors
if self.application_type == "bookwyrm":
connector_model = apps.get_model("bookwyrm.Connector", require_ready=True)
connector_model.objects.filter(
identifier=self.server_name,
active=False,
deactivation_reason="domain_block",
).update(active=True, deactivation_reason=None)
@classmethod @classmethod
def is_blocked(cls, url): def is_blocked(cls, url):
"""look up if a domain is blocked""" """look up if a domain is blocked"""

View file

@ -19,21 +19,11 @@ from bookwyrm.signatures import create_key_pair
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.utils import regex from bookwyrm.utils import regex
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel, DeactivationReason
from .federated_server import FederatedServer from .federated_server import FederatedServer
from . import fields, Review from . import fields, Review
DeactivationReason = models.TextChoices(
"DeactivationReason",
[
"self_deletion",
"moderator_deletion",
"domain_block",
],
)
class User(OrderedCollectionPageMixin, AbstractUser): class User(OrderedCollectionPageMixin, AbstractUser):
"""a user who wants to read books""" """a user who wants to read books"""

View file

@ -1,40 +0,0 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{{ author.name }}{% endblock %}
{% block content %}
<div class="block">
<div class="columns is-mobile">
<div class="column">
<h1 class="title">{{ author.name }}</h1>
</div>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
<div class="column is-narrow">
<a href="{{ author.local_path }}/edit">
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}">
<span class="is-sr-only">{% trans "Edit Author" %}</span>
</span>
</a>
</div>
{% endif %}
</div>
</div>
<div class="block content">
{% if author.bio %}
{{ author.bio | to_markdown | safe }}
{% endif %}
{% if author.wikipedia_link %}
<p><a href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">{% trans "Wikipedia" %}</a></p>
{% endif %}
</div>
<div class="block">
<h3 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h3>
{% include 'snippets/book_tiles.html' with books=books %}
</div>
{% endblock %}

View file

@ -0,0 +1,83 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load markdown %}
{% load humanize %}
{% block title %}{{ author.name }}{% endblock %}
{% block content %}
<div class="block">
<div class="columns is-mobile">
<div class="column">
<h1 class="title">{{ author.name }}</h1>
</div>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
<div class="column is-narrow">
<a href="{{ author.local_path }}/edit">
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
<span>{% trans "Edit Author" %}</span>
</a>
</div>
{% endif %}
</div>
</div>
<div class="block content columns">
{% if author.aliases or author.born or author.died or author.wikipedia_link %}
<div class="column is-narrow">
<div class="box">
<dl>
{% if author.aliases %}
<div class="is-flex">
<dt class="mr-1">{% trans "Aliases:" %}</dt>
<dd itemprop="aliases">{{ author.aliases|join:', ' }}</dd>
</div>
{% endif %}
{% if author.born %}
<div class="is-flex">
<dt class="mr-1">{% trans "Born:" %}</dt>
<dd itemprop="aliases">{{ author.born|naturalday }}</dd>
</div>
{% endif %}
{% if author.aliases %}
<div class="is-flex">
<dt class="mr-1">{% trans "Died:" %}</dt>
<dd itemprop="aliases">{{ author.died|naturalday }}</dd>
</div>
{% endif %}
</dl>
{% if author.wikipedia_link %}
<p><a href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">{% trans "Wikipedia" %}</a></p>
{% endif %}
{% if author.openlibrary_key %}
<p class="mb-0">
<a href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a>
</p>
{% endif %}
{% if author.inventaire_id %}
<p class="mb-0">
<a href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a>
</p>
{% endif %}
</div>
</div>
{% endif %}
<div class="column">
{% if author.bio %}
{{ author.bio|to_markdown|safe }}
{% endif %}
</div>
</div>
<div class="block">
<h3 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h3>
<div class="columns is-multiline is-mobile">
{% for book in books %}
<div class="column is-one-fifth">
{% include 'discover/small-book.html' with book=book %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -29,44 +29,64 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <h2 class="title is-4">{% trans "Metadata" %}</h2>
<p><label class="label" for="id_name">{% trans "Name:" %}</label> {{ form.name }}</p> <p class="mb-2"><label class="label" for="id_name">{% trans "Name:" %}</label> {{ form.name }}</p>
{% for error in form.name.errors %} {% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p><label class="label" for="id_bio">{% trans "Bio:" %}</label> {{ form.bio }}</p> <p class="mb-2">
<label class="label" for="id_aliases">{% trans "Aliases:" %}</label>
{{ form.aliases }}
<span class="help">{% trans "Separate multiple values with commas." %}</span>
</p>
{% for error in form.aliases.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_bio">{% trans "Bio:" %}</label> {{ form.bio }}</p>
{% for error in form.bio.errors %} {% for error in form.bio.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p> <p class="mb-2"><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
{% for error in form.wikipedia_link.errors %} {% for error in form.wikipedia_link.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p><label class="label" for="id_born">{% trans "Birth date:" %}</label> {{ form.born }}</p> <p class="mb-2">
<label class="label" for="id_born">{% trans "Birth date:" %}</label>
<input type="date" name="born" value="{{ form.born.value|date:'Y-m-d' }}" class="input" id="id_born">
</p>
{% for error in form.born.errors %} {% for error in form.born.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p><label class="label" for="id_died">{% trans "Death date:" %}</label> {{ form.died }}</p> <p class="mb-2">
<label class="label" for="id_died">{% trans "Death date:" %}</label>
<input type="date" name="died" value="{{ form.died.value|date:'Y-m-d' }}" class="input" id="id_died">
</p>
{% for error in form.died.errors %} {% for error in form.died.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="column"> <div class="column">
<h2 class="title is-4">{% trans "Author Identifiers" %}</h2> <h2 class="title is-4">{% trans "Author Identifiers" %}</h2>
<p><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }}</p> <p class="mb-2"><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }}</p>
{% for error in form.openlibrary_key.errors %} {% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p><label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label> {{ form.librarything_key }}</p> <p class="mb-2"><label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label> {{ form.inventaire_id }}</p>
{% for error in form.inventaire_id.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label> {{ form.librarything_key }}</p>
{% for error in form.librarything_key.errors %} {% for error in form.librarything_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p><label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label> {{ form.goodreads_key }}</p> <p class="mb-2"><label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label> {{ form.goodreads_key }}</p>
{% for error in form.goodreads_key.errors %} {% for error in form.goodreads_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}

View file

@ -38,9 +38,8 @@
{% if user_authenticated and can_edit_book %} {% if user_authenticated and can_edit_book %}
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{{ book.id }}/edit"> <a href="{{ book.id }}/edit">
<span class="icon icon-pencil" title="{% trans "Edit Book" %}"> <span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
<span class="is-sr-only">{% trans "Edit Book" %}</span> <span>{% trans "Edit Book" %}</span>
</span>
</a> </a>
</div> </div>
{% endif %} {% endif %}
@ -163,12 +162,9 @@
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Add read dates" as button_text %} {% trans "Add read dates" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="plus" class="is-small" controls_text="add-readthrough" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add-readthrough" focus="add-readthrough-focus" %}
</div> </div>
</header> </header>
{% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
<section class="is-hidden box" id="add-readthrough"> <section class="is-hidden box" id="add-readthrough">
<form name="add-readthrough" action="/create-readthrough" method="post"> <form name="add-readthrough" action="/create-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=None %} {% include 'snippets/readthrough_form.html' with readthrough=None %}
@ -183,6 +179,9 @@
</div> </div>
</form> </form>
</section> </section>
{% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
{% for readthrough in readthroughs %} {% for readthrough in readthroughs %}
{% include 'snippets/readthrough.html' with readthrough=readthrough %} {% include 'snippets/readthrough.html' with readthrough=readthrough %}
{% endfor %} {% endfor %}
@ -195,7 +194,7 @@
{% endif %} {% endif %}
<div class="block" id="reviews"> <div class="block" id="reviews">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{% if user_statuses.review_count or user_statuses.comment_count or user_stuatses.quotation_count %} {% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
<nav class="tabs"> <nav class="tabs">
<ul> <ul>
{% url 'book' book.id as tab_url %} {% url 'book' book.id as tab_url %}
@ -225,46 +224,26 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% for review in statuses %} {% for status in statuses %}
<div <div
class="block" class="block"
{% if status.status_type == 'Review' or status.status_type == 'Rating' %}
itemprop="review" itemprop="review"
itemscope itemscope
itemtype="https://schema.org/Review" itemtype="https://schema.org/Review"
{% endif %}
> >
{% with status=review hide_book=True depth=1 %} {% include 'snippets/status/status.html' with status=status hide_book=True depth=1 %}
{% include 'snippets/status/status.html' %}
{% endwith %}
</div> </div>
{% endfor %} {% endfor %}
{% if ratings %}
<div class="block is-flex is-flex-wrap-wrap"> <div class="block is-flex is-flex-wrap-wrap">
{% for rating in ratings %} {% for rating in ratings %}
{% with user=rating.user %} {% include 'book/rating.html' with user=rating.user rating=rating %}
<div class="block mr-5">
<div class="media">
<div class="media-left">
{% include 'snippets/avatar.html' %}
</div>
<div class="media-content">
<div>
<a href="{{ user.local_path }}">{{ user.display_name }}</a>
</div>
<div class="is-flex">
<p class="mr-1">{% trans "rated it" %}</p>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
</div>
</div>
</div>
</div>
{% endwith %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
<div class="block"> <div class="block">
{% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %} {% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
</div> </div>

View file

@ -125,7 +125,7 @@
<p class="mb-2"> <p class="mb-2">
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label> <label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
{{ form.publishers }} {{ form.publishers }}
<span class="help">{% trans "Separate multiple publishers with commas." %}</span> <span class="help">{% trans "Separate multiple values with commas." %}</span>
</p> </p>
{% for error in form.publishers.errors %} {% for error in form.publishers.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -162,7 +162,7 @@
{% endif %} {% endif %}
<label class="label" for="id_add_author">{% trans "Add Authors:" %}</label> <label class="label" for="id_add_author">{% trans "Add Authors:" %}</label>
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}> <input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
<p class="help">Separate multiple author names with commas.</p> <span class="help">{% trans "Separate multiple values with commas." %}</span>
</section> </section>
</div> </div>

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{% blocktrans with book_title=work.title %}Editions of {{ book_title }}{% endblocktrans %}{% endblock %} {% block title %}{% blocktrans with book_title=work.title %}Editions of {{ book_title }}{% endblocktrans %}{% endblock %}

View file

@ -0,0 +1,22 @@
{% load i18n %}{% load status_display %}
<div class="block mr-5">
<div class="media">
<div class="media-left">
{% include 'snippets/avatar.html' with user=user %}
</div>
<div class="media-content">
<div>
<a href="{{ user.local_path }}">{{ user.display_name }}</a>
</div>
<div class="is-flex">
<p class="mr-1">{% trans "rated it" %}</p>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>
<a href="{{ rating.remote_id }}">{{ rating.published_date|published_date }}</a>
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
{% spaceless %} {% spaceless %}
{% load bookwyrm_tags %} {% load utilities %}
{% with 0|uuid as uuid %} {% with 0|uuid as uuid %}
<div <div

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% block title %}{% trans "Compose status" %}{% endblock %} {% block title %}{% trans "Compose status" %}{% endblock %}
{% block content %} {% block content %}

View file

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% load markdown %}
{% load humanize %} {% load humanize %}
<div class="card is-stretchable"> <div class="card is-stretchable">
@ -19,7 +20,7 @@
<div> <div>
{% if user.summary %} {% if user.summary %}
{{ user.summary | to_markdown | safe | truncatechars_html:40 }} {{ user.summary|to_markdown|safe|truncatechars_html:40 }}
{% else %}&nbsp;{% endif %} {% else %}&nbsp;{% endif %}
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load markdown %}
{% block title %}{% trans "Welcome" %}{% endblock %} {% block title %}{% trans "Welcome" %}{% endblock %}
@ -49,7 +49,7 @@
{% else %} {% else %}
<h2 class="title">{% trans "This instance is closed" %}</h2> <h2 class="title">{% trans "This instance is closed" %}</h2>
<p>{{ site.registration_closed_text | safe}}</p> <p>{{ site.registration_closed_text|safe}}</p>
{% if site.allow_invite_requests %} {% if site.allow_invite_requests %}
{% if request_received %} {% if request_received %}
@ -64,7 +64,7 @@
<label for="id_request_email" class="label">{% trans "Email address:" %}</label> <label for="id_request_email" class="label">{% trans "Email address:" %}</label>
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email"> <input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
{% for error in request_form.email.errors %} {% for error in request_form.email.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error|escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
<button type="submit" class="button is-link">{% trans "Submit" %}</button> <button type="submit" class="button is-link">{% trans "Submit" %}</button>
@ -80,7 +80,7 @@
{% include 'user/user_preview.html' with user=request.user %} {% include 'user/user_preview.html' with user=request.user %}
{% if request.user.summary %} {% if request.user.summary %}
<div class="box content"> <div class="box content">
{{ request.user.summary | to_markdown | safe }} {{ request.user.summary|to_markdown|safe }}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,5 +1,5 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %}
{% load i18n %} {% load i18n %}
{% if book %} {% if book %}

View file

@ -1,7 +1,6 @@
{% extends 'feed/feed_layout.html' %} {% extends 'feed/feed_layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block panel %} {% block panel %}
<h1 class="title"> <h1 class="title">

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{% trans "Updates" %}{% endblock %} {% block title %}{% trans "Updates" %}{% endblock %}

View file

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% load humanize %} {% load humanize %}
<div class="columns is-mobile scroll-x mb-0"> <div class="columns is-mobile scroll-x mb-0">
{% for user in suggested_users %} {% for user in suggested_users %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %} {% load status_display %}
<div class="block"> <div class="block">
{% with depth=depth|add:1 %} {% with depth=depth|add:1 %}

View file

@ -9,7 +9,7 @@
{% if is_self and goal %} {% if is_self and goal %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Edit Goal" as button_text %} {% trans "Edit Goal" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="show-edit-goal" focus="edit-form-header" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="show-edit-goal" focus="edit-form-header" %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %} {% load humanize %}
{% block title %}{% trans "Import Status" %}{% endblock %} {% block title %}{% trans "Import Status" %}{% endblock %}
@ -54,8 +53,8 @@
<input class="checkbox" type="checkbox" name="import_item" value="{{ item.id }}" id="import-item-{{ item.id }}"> <input class="checkbox" type="checkbox" name="import_item" value="{{ item.id }}" id="import-item-{{ item.id }}">
<label for="import-item-{{ item.id }}"> <label for="import-item-{{ item.id }}">
Line {{ item.index }}: Line {{ item.index }}:
<strong>{{ item.data|dict_key:'Title' }}</strong> by <strong>{{ item.data.Title }}</strong> by
{{ item.data|dict_key:'Author' }} {{ item.data.Author }}
</label> </label>
<p> <p>
{{ item.fail_reason }}. {{ item.fail_reason }}.
@ -90,8 +89,8 @@
<li class="pb-1"> <li class="pb-1">
<p> <p>
Line {{ item.index }}: Line {{ item.index }}:
<strong>{{ item.data|dict_key:'Title' }}</strong> by <strong>{{ item.data.Title }}</strong> by
{{ item.data|dict_key:'Author' }} {{ item.data.Author }}
</p> </p>
<p> <p>
{{ item.fail_reason }}. {{ item.fail_reason }}.
@ -130,10 +129,10 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ item.data|dict_key:'Title' }} {{ item.data.Title }}
</td> </td>
<td> <td>
{{ item.data|dict_key:'Author' }} {{ item.data.Author }}
</td> </td>
<td> <td>
{% if item.book %} {% if item.book %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %} {% load layout %}
{% load i18n %} {% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{% get_lang %}"> <html lang="{% get_lang %}">
@ -214,7 +214,7 @@
</p> </p>
{% endif %} {% endif %}
<p> <p>
{% trans 'BookWyrm is open source software. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.' %} {% blocktrans %}BookWyrm's source code is freely available. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.{% endblocktrans %}
</p> </p>
</div> </div>
{% if site.footer_item %} {% if site.footer_item %}

View file

@ -1,12 +1,13 @@
{% extends 'lists/list_layout.html' %} {% extends 'lists/list_layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %}
{% block panel %} {% block panel %}
{% if request.user == list.user and pending_count %} {% if request.user == list.user and pending_count %}
<div class="block content"> <div class="block content">
<p> <p>
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count | pluralize }} awaiting your approval</a> <a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count|pluralize }} awaiting your approval</a>
</p> </p>
</div> </div>
{% endif %} {% endif %}
@ -50,9 +51,9 @@
<p> <p>
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %} {% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
</p> </p>
<p> <div>
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }} {{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
</p> </div>
{% include 'snippets/shelve_button/shelve_button.html' %} {% include 'snippets/shelve_button/shelve_button.html' %}
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %} {% load markdown %}
<div class="columns is-multiline"> <div class="columns is-multiline">
{% for list in lists %} {% for list in lists %}
<div class="column is-one-quarter"> <div class="column is-one-quarter">
@ -22,7 +22,7 @@
{% endwith %} {% endwith %}
<div class="card-content is-flex-grow-0"> <div class="card-content is-flex-grow-0">
<div {% if list.description %}title="{{ list.description }}"{% endif %}> <div class="is-clipped" {% if list.description %}title="{{ list.description }}"{% endif %}>
{% if list.description %} {% if list.description %}
{{ list.description|to_markdown|safe|truncatechars_html:30 }} {{ list.description|to_markdown|safe|truncatechars_html:30 }}
{% else %} {% else %}

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{{ list.name }}{% endblock %} {% block title %}{{ list.name }}{% endblock %}
@ -16,7 +15,7 @@
{% if request.user == list.user %} {% if request.user == list.user %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Edit List" as button_text %} {% trans "Edit List" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-list" focus="edit-list-header" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit-list" focus="edit-list-header" %}
</div> </div>
{% endif %} {% endif %}
</header> </header>

View file

@ -1,5 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load bookwyrm_tags %} {% load utilities %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Lists" %}{% endblock %} {% block title %}{% trans "Lists" %}{% endblock %}
@ -18,7 +18,7 @@
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Create List" as button_text %} {% trans "Create List" as button_text %}
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon="plus" text=button_text focus="create-list-header" %} {% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon_with_text="plus" text=button_text focus="create-list-header" %}
</div> </div>
{% endif %} {% endif %}
</header> </header>

View file

@ -38,6 +38,9 @@
</div> </div>
</form> </form>
</div> </div>
</div>
<div class="column">
<div class="box has-background-primary-light"> <div class="box has-background-primary-light">
{% if site.allow_registration %} {% if site.allow_registration %}
<h2 class="title">{% trans "Create an Account" %}</h2> <h2 class="title">{% trans "Create an Account" %}</h2>
@ -50,15 +53,15 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<div class="column"> <div class="block">
<div class="block"> <div class="box">
{% include 'snippets/about.html' %} {% include 'snippets/about.html' %}
<p class="block"> <p class="block">
<a href="{% url 'about' %}">{% trans "More about this site" %}</a> <a href="{% url 'about' %}">{% trans "More about this site" %}</a>
</p> </p>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,6 +1,5 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/admin_layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %} {% load humanize %}
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %} {% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
@ -29,7 +28,7 @@
<a href="{{ comment.user.local_path }}">{{ comment.user.display_name }}</a> <a href="{{ comment.user.local_path }}">{{ comment.user.display_name }}</a>
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
{{ comment.created_date | naturaltime }} {{ comment.created_date|naturaltime }}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{% trans "Notifications" %}{% endblock %} {% block title %}{% trans "Notifications" %}{% endblock %}

View file

@ -7,7 +7,7 @@
{% with results|first as local_results %} {% with results|first as local_results %}
<ul class="block"> <ul class="block">
{% for result in local_results.results %} {% for result in local_results.results %}
<li class="pd-4"> <li class="pd-4 mb-5">
{% include 'snippets/search_result_text.html' with result=result %} {% include 'snippets/search_result_text.html' with result=result %}
</li> </li>
{% endfor %} {% endfor %}

View file

@ -1,5 +1,4 @@
{% extends 'search/layout.html' %} {% extends 'search/layout.html' %}
{% load bookwyrm_tags %}
{% block panel %} {% block panel %}

View file

@ -1,7 +1,8 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/admin_layout.html' %}
{% block title %}{{ server.server_name }}{% endblock %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load markdown %}
{% block title %}{{ server.server_name }}{% endblock %}
{% block header %} {% block header %}
{{ server.server_name }} {{ server.server_name }}
@ -14,60 +15,64 @@
{% block panel %} {% block panel %}
<div class="columns"> <div class="columns">
<section class="column is-half content"> <section class="column is-half is-flex is-flex-direction-column">
<h2 class="title is-4">{% trans "Details" %}</h2> <h2 class="title is-4">{% trans "Details" %}</h2>
<dl> <div class="box is-flex-grow-1 content">
<div class="is-flex"> <dl>
<dt>{% trans "Software:" %}</dt> <div class="is-flex">
<dd>{{ server.application_type }}</dd> <dt>{% trans "Software:" %}</dt>
</div> <dd>{{ server.application_type }}</dd>
<div class="is-flex"> </div>
<dt>{% trans "Version:" %}</dt> <div class="is-flex">
<dd>{{ server.application_version }}</dd> <dt>{% trans "Version:" %}</dt>
</div> <dd>{{ server.application_version }}</dd>
<div class="is-flex"> </div>
<dt>{% trans "Status:" %}</dt> <div class="is-flex">
<dd>{{ server.status }}</dd> <dt>{% trans "Status:" %}</dt>
</div> <dd>{{ server.status }}</dd>
</dl> </div>
</dl>
</div>
</section> </section>
<section class="column is-half content"> <section class="column is-half is-flex is-flex-direction-column">
<h2 class="title is-4">{% trans "Activity" %}</h2> <h2 class="title is-4">{% trans "Activity" %}</h2>
<dl> <div class="box is-flex-grow-1 content">
<div class="is-flex"> <dl>
<dt>{% trans "Users:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Users:" %}</dt>
{{ users.count }} <dd>
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %} {{ users.count }}
</dd> {% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
</div> </dd>
<div class="is-flex"> </div>
<dt>{% trans "Reports:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Reports:" %}</dt>
{{ reports.count }} <dd>
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %} {{ reports.count }}
</dd> {% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
</div> </dd>
<div class="is-flex"> </div>
<dt>{% trans "Followed by us:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Followed by us:" %}</dt>
{{ followed_by_us.count }} <dd>
</dd> {{ followed_by_us.count }}
</div> </dd>
<div class="is-flex"> </div>
<dt>{% trans "Followed by them:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Followed by them:" %}</dt>
{{ followed_by_them.count }} <dd>
</dd> {{ followed_by_them.count }}
</div> </dd>
<div class="is-flex"> </div>
<dt>{% trans "Blocked by us:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Blocked by us:" %}</dt>
{{ blocked_by_us.count }} <dd>
</dd> {{ blocked_by_us.count }}
</div> </dd>
</dl> </div>
</dl>
</div>
</section> </section>
</div> </div>
@ -78,11 +83,11 @@
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Edit" as button_text %} {% trans "Edit" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-notes" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit-notes" %}
</div> </div>
</header> </header>
{% if server.notes %} {% if server.notes %}
<p id="hide-edit-notes">{{ server.notes|to_markdown|safe }}</p> <div class="box" id="hide-edit-notes">{{ server.notes|to_markdown|safe }}</div>
{% endif %} {% endif %}
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes"> <form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes">
{% csrf_token %} {% csrf_token %}

View file

@ -6,9 +6,8 @@
{% block edit-button %} {% block edit-button %}
<a href="{% url 'settings-import-blocklist' %}"> <a href="{% url 'settings-import-blocklist' %}">
<span class="icon icon-plus" title="{% trans 'Add server' %}"> <span class="icon icon-plus" title="{% trans 'Add server' %}" aria-hidden="True"></span>
<span class="is-sr-only">{% trans "Add server" %}</span> <span>{% trans "Add server" %}</span>
</span>
</a> </a>
{% endblock %} {% endblock %}

View file

@ -1,3 +1,2 @@
{% load bookwyrm_tags %}
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}"> <img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">

View file

@ -1,6 +1,5 @@
{% spaceless %} {% spaceless %}
{% load bookwyrm_tags %}
{% load i18n %} {% load i18n %}
<figure <figure

View file

@ -1,16 +0,0 @@
<div class="columns is-mobile is-multiline">
{% for book in books %}
<div class="column is-narrow">
<div class="box is-flex is-flex-direction-column is-align-items-center">
<div class="mb-3">
<a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-l-mobile is-h-l-mobile is-w-l-tablet is-h-xl-tablet' %}
</a>
</div>
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
</div>
</div>
{% endfor %}
</div>

View file

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% if book.authors %} {% if book.authors %}
{% blocktrans with path=book.local_path title=book|title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %} {% blocktrans with path=book.local_path title=book|title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
{% else %} {% else %}

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %} {% load interaction %}
{% load utilities %}
{% load i18n %} {% load i18n %}
{% with status.id|uuid as uuid %} {% with status.id|uuid as uuid %}

View file

@ -1,6 +1,6 @@
{% load humanize %} {% load humanize %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% with status_type=request.GET.status_type %} {% with status_type=request.GET.status_type %}
<div class="tab-group"> <div class="tab-group">

View file

@ -1,4 +1,7 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load utilities %}
{% load status_display %}
{% load i18n %} {% load i18n %}
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{% if type == 'direct' %}status{% else %}{{ type }}{% endif %}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}"> <form class="is-flex-grow-1" name="{{ type }}" action="/post/{% if type == 'direct' %}status{% else %}{{ type }}{% endif %}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}">
{% csrf_token %} {% csrf_token %}
@ -100,7 +103,7 @@
{# bottom bar #} {# bottom bar #}
<input type="checkbox" class="is-hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true"> <input type="checkbox" class="is-hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true">
<div class="columns mt-1"> <div class="columns mt-1">
<div class="field has-addons column"> <div class="field has-addons column">
<div class="control"> <div class="control">

View file

@ -1,5 +1,7 @@
{% load bookwyrm_tags %} {% load interaction %}
{% load utilities %}
{% load i18n %} {% load i18n %}
{% with status.id|uuid as uuid %} {% with status.id|uuid as uuid %}
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}"> <form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %} {% csrf_token %}

View file

@ -5,9 +5,9 @@
<span class="column is-narrow pb-0"> <span class="column is-narrow pb-0">
{% trans "Show filters" as text %} {% trans "Show filters" as text %}
{% include 'snippets/toggle/open_button.html' with text=text controls_text="filters" icon="arrow-down" class="is-small" focus="filters" %} {% include 'snippets/toggle/open_button.html' with text=text controls_text="filters" icon_with_text="arrow-down" class="is-small" focus="filters" %}
{% trans "Hide filters" as text %} {% trans "Hide filters" as text %}
{% include 'snippets/toggle/close_button.html' with text=text controls_text="filters" icon="x" class="is-small" %} {% include 'snippets/toggle/close_button.html' with text=text controls_text="filters" icon_with_text="arrow-up" class="is-small" %}
</span> </span>
</h2> </h2>

View file

@ -1,6 +1,5 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% if request.user in user.follow_requests.all %}
{% if request.user|follow_request_exists:user %}
<div class="field is-grouped"> <div class="field is-grouped">
<form action="/accept-follow-request/" method="POST"> <form action="/accept-follow-request/" method="POST">
{% csrf_token %} {% csrf_token %}

View file

@ -1,6 +1,5 @@
{% spaceless %} {% spaceless %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
<div class=" <div class="
field is-grouped field is-grouped

View file

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
<div class="select {{ class }}"> <div class="select {{ class }}">
{% with 0|uuid as uuid %} {% with 0|uuid as uuid %}
{% if not no_label %} {% if not no_label %}

View file

@ -3,7 +3,7 @@
<input type="hidden" name="id" value="{{ readthrough.id }}"> <input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<div class="field"> <div class="field">
<label class="label"> <label class="label" tabindex="0" id="add-readthrough-focus">
{% trans "Started reading" %} {% trans "Started reading" %}
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}"> <input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</label> </label>

View file

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% with 0|uuid as report_uuid %} {% with 0|uuid as report_uuid %}
{% trans "Report" as button_text %} {% trans "Report" as button_text %}

View file

@ -1,4 +1,6 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load utilities %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{% with book.id|uuid as uuid %} {% with book.id|uuid as uuid %}

View file

@ -1,5 +1,7 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load utilities %}
{% load i18n %} {% load i18n %}
{% for shelf in shelves %} {% for shelf in shelves %}
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %} {% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
{% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %} {% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %}

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %}
{% load i18n %} {% load i18n %}
{% with status_type=status.status_type %} {% with status_type=status.status_type %}

View file

@ -1,6 +1,7 @@
{% spaceless %} {% spaceless %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %}
{% load i18n %} {% load i18n %}
{% if not hide_book %} {% if not hide_book %}

View file

@ -1,7 +1,6 @@
{% extends 'components/card.html' %} {% extends 'components/card.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% load humanize %}
{% block card-header %} {% block card-header %}
<div class="card-header-title has-background-white-ter is-block"> <div class="card-header-title has-background-white-ter is-block">

View file

@ -1,5 +1,6 @@
{% load bookwyrm_tags %} {% load status_display %}
{% load i18n %} {% load i18n %}
{% if not status.deleted %} {% if not status.deleted %}
{% if status.status_type == 'Announce' %} {% if status.status_type == 'Announce' %}
<a href="{{ status.user.local_path }}"> <a href="{{ status.user.local_path }}">

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %} {% load utilities %}
{% load status_display %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
@ -29,7 +30,7 @@
</span> </span>
{% if status.status_type == 'GeneratedNote' %} {% if status.status_type == 'GeneratedNote' %}
{{ status.content | safe }} {{ status.content|safe }}
{% elif status.status_type == 'Rating' %} {% elif status.status_type == 'Rating' %}
{% trans "rated" %} {% trans "rated" %}
{% elif status.status_type == 'Review' %} {% elif status.status_type == 'Review' %}
@ -91,7 +92,7 @@
</h3> </h3>
<p class="is-size-7 is-flex is-align-items-center"> <p class="is-size-7 is-flex is-align-items-center">
<a href="{{ status.remote_id }}">{{ status.published_date|timesince }}</a> <a href="{{ status.remote_id }}">{{ status.published_date|published_date }}</a>
{% if status.progress %} {% if status.progress %}
<span class="ml-1"> <span class="ml-1">
{% if status.progress_mode == 'PG' %} {% if status.progress_mode == 'PG' %}

View file

@ -1,6 +1,6 @@
{% extends 'components/dropdown.html' %} {% extends 'components/dropdown.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% block dropdown-trigger %} {% block dropdown-trigger %}
<span class="icon icon-dots-three m-0-mobile"></span> <span class="icon icon-dots-three m-0-mobile"></span>

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %} {% load markdown %}
{% load utilities %}
{% load i18n %} {% load i18n %}
{% with 0|uuid as uuid %} {% with 0|uuid as uuid %}

View file

@ -1,6 +1,6 @@
{% extends 'components/dropdown.html' %} {% extends 'components/dropdown.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% block dropdown-trigger %} {% block dropdown-trigger %}
<span class="icon icon-dots-three"> <span class="icon icon-dots-three">

View file

@ -1,7 +1,8 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load bookwyrm_tags %} {% load utilities %}
{% load markdown %}
{% block title %}{{ user.display_name }}{% endblock %} {% block title %}{{ user.display_name }}{% endblock %}
@ -23,7 +24,7 @@
{% if user.summary %} {% if user.summary %}
<div class="column box has-background-white-bis content"> <div class="column box has-background-white-bis content">
{{ user.summary | to_markdown | safe }} {{ user.summary|to_markdown|safe }}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -15,7 +15,7 @@
{% if is_self %} {% if is_self %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Create list" as button_text %} {% trans "Create list" as button_text %}
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon="plus" text=button_text %} {% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon_with_text="plus" text=button_text %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,6 +1,6 @@
{% extends 'user/layout.html' %} {% extends 'user/layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% block tabs %} {% block tabs %}
{% with user|username as username %} {% with user|username as username %}

View file

@ -1,5 +1,6 @@
{% extends 'user/layout.html' %} {% extends 'user/layout.html' %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load utilities %}
{% load humanize %} {% load humanize %}
{% load i18n %} {% load i18n %}
@ -35,7 +36,7 @@
{% if is_self %} {% if is_self %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Create shelf" as button_text %} {% trans "Create shelf" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="plus" controls_text="create-shelf-form" focus="create-shelf-form-header" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" controls_text="create-shelf-form" focus="create-shelf-form-header" %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -58,7 +59,7 @@
{% if is_self and shelf.id %} {% if is_self and shelf.id %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Edit shelf" as button_text %} {% trans "Edit shelf" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-shelf-form" focus="edit-shelf-form-header" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit-shelf-form" focus="edit-shelf-form-header" %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -79,7 +80,9 @@
<th>{% trans "Shelved" %}</th> <th>{% trans "Shelved" %}</th>
<th>{% trans "Started" %}</th> <th>{% trans "Started" %}</th>
<th>{% trans "Finished" %}</th> <th>{% trans "Finished" %}</th>
{% if ratings %}<th>{% trans "Rating" %}</th>{% endif %} {% if request.user.is_authenticated %}
<th>{% trans "Rating" %}</th>
{% endif %}
{% if shelf.user == request.user %} {% if shelf.user == request.user %}
<th aria-hidden="true"></th> <th aria-hidden="true"></th>
{% endif %} {% endif %}
@ -99,18 +102,18 @@
{% include 'snippets/authors.html' %} {% include 'snippets/authors.html' %}
</td> </td>
<td data-title="{% trans "Shelved" %}"> <td data-title="{% trans "Shelved" %}">
{{ book.created_date | naturalday }} {{ book.created_date|naturalday }}
</td> </td>
{% latest_read_through book user as read_through %} {% latest_read_through book user as read_through %}
<td data-title="{% trans "Started" %}"> <td data-title="{% trans "Started" %}">
{{ read_through.start_date | naturalday |default_if_none:""}} {{ read_through.start_date|naturalday|default_if_none:""}}
</td> </td>
<td data-title="{% trans "Finished" %}"> <td data-title="{% trans "Finished" %}">
{{ read_through.finish_date | naturalday |default_if_none:""}} {{ read_through.finish_date|naturalday|default_if_none:""}}
</td> </td>
{% if ratings %} {% if request.user.is_authenticated %}
<td data-title="{% trans "Rating" %}"> <td data-title="{% trans "Rating" %}">
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %} {% include 'snippets/stars.html' with rating=book.rating %}
</td> </td>
{% endif %} {% endif %}
{% if shelf.user == request.user %} {% if shelf.user == request.user %}

View file

@ -1,6 +1,6 @@
{% extends 'user/layout.html' %} {% extends 'user/layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% block title %}{{ user.display_name }}{% endblock %} {% block title %}{{ user.display_name }}{% endblock %}
@ -12,9 +12,8 @@
{% if is_self %} {% if is_self %}
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{% url 'prefs-profile' %}"> <a href="{% url 'prefs-profile' %}">
<span class="icon icon-pencil" title="Edit profile"> <span class="icon icon-pencil" title="Edit profile" aria-hidden="true"></span>
<span class="is-sr-only">{% trans "Edit profile" %}</span> <span>{% trans "Edit profile" %}</span>
</span>
</a> </a>
</div> </div>
{% endif %} {% endif %}
@ -59,8 +58,9 @@
<div class="columns is-mobile"> <div class="columns is-mobile">
<h2 class="title column">{% trans "User Activity" %}</h2> <h2 class="title column">{% trans "User Activity" %}</h2>
<div class="column is-narrow"> <div class="column is-narrow">
<a class="icon icon-rss" target="_blank" href="{{ user.local_path }}/rss"> <a target="_blank" href="{{ user.local_path }}/rss">
<span class="is-sr-only">{% trans "RSS feed" %}</span> <span class="icon icon-rss" aria-hidden="true"></span>
<span>{% trans "RSS feed" %}</span>
</a> </a>
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load utilities %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
<div class="media block"> <div class="media block">

View file

@ -1,7 +1,5 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/admin_layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{{ user.username }}{% endblock %} {% block title %}{{ user.username }}{% endblock %}
{% block header %}{{ user.username }}{% endblock %} {% block header %}{{ user.username }}{% endblock %}

View file

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load markdown %}
<div class="block columns"> <div class="block columns">
<div class="column is-flex is-flex-direction-column"> <div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4> <h4 class="title is-4">{% trans "User details" %}</h4>
@ -7,7 +7,7 @@
{% include 'user/user_preview.html' with user=user %} {% include 'user/user_preview.html' with user=user %}
{% if user.summary %} {% if user.summary %}
<div class="box content has-background-white-ter is-shadowless"> <div class="box content has-background-white-ter is-shadowless">
{{ user.summary | to_markdown | safe }} {{ user.summary|to_markdown|safe }}
</div> </div>
{% endif %} {% endif %}

View file

@ -1,22 +1,15 @@
""" template filters """ """ template filters """
from uuid import uuid4 from django import template
from django import template, utils
from django.db.models import Avg from django.db.models import Avg
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.views.status import to_markdown from bookwyrm.views.status import to_markdown
from bookwyrm.templatetags.utilities import get_user_identifier
register = template.Library() register = template.Library()
@register.filter(name="dict_key")
def dict_key(d, k):
"""Returns the given key from a dictionary."""
return d.get(k) or 0
@register.filter(name="rating") @register.filter(name="rating")
def get_rating(book, user): def get_rating(book, user):
"""get the overall rating of a book""" """get the overall rating of a book"""
@ -43,119 +36,12 @@ def get_user_rating(book, user):
return 0 return 0
@register.filter(name="username")
def get_user_identifier(user):
"""use localname for local users, username for remote"""
return user.localname if user.localname else user.username
@register.filter(name="notification_count")
def get_notification_count(user):
"""how many UNREAD notifications are there"""
return user.notification_set.filter(read=False).count()
@register.filter(name="replies")
def get_replies(status):
"""get all direct replies to a status"""
# TODO: this limit could cause problems
return models.Status.objects.filter(
reply_parent=status,
deleted=False,
).select_subclasses()[:10]
@register.filter(name="parent")
def get_parent(status):
"""get the reply parent for a status"""
return (
models.Status.objects.filter(id=status.reply_parent_id)
.select_subclasses()
.get()
)
@register.filter(name="liked")
def get_user_liked(user, status):
"""did the given user fav a status?"""
try:
models.Favorite.objects.get(user=user, status=status)
return True
except models.Favorite.DoesNotExist:
return False
@register.filter(name="boosted")
def get_user_boosted(user, status):
"""did the given user fav a status?"""
return user.id in status.boosters.all().values_list("user", flat=True)
@register.filter(name="follow_request_exists")
def follow_request_exists(user, requester):
"""see if there is a pending follow request for a user"""
try:
models.UserFollowRequest.objects.filter(
user_subject=requester,
user_object=user,
).get()
return True
except models.UserFollowRequest.DoesNotExist:
return False
@register.filter(name="boosted_status")
def get_boosted(boost):
"""load a boosted status. have to do this or it wont get foregin keys"""
return (
models.Status.objects.select_subclasses()
.filter(id=boost.boosted_status.id)
.get()
)
@register.filter(name="book_description") @register.filter(name="book_description")
def get_book_description(book): def get_book_description(book):
"""use the work's text if the book doesn't have it""" """use the work's text if the book doesn't have it"""
return book.description or book.parent_work.description return book.description or book.parent_work.description
@register.filter(name="uuid")
def get_uuid(identifier):
"""for avoiding clashing ids when there are many forms"""
return "%s%s" % (identifier, uuid4())
@register.filter(name="to_markdown")
def get_markdown(content):
"""convert markdown to html"""
if content:
return to_markdown(content)
return None
@register.filter(name="mentions")
def get_mentions(status, user):
"""people to @ in a reply: the parent and all mentions"""
mentions = set([status.user] + list(status.mention_users.all()))
return (
" ".join("@" + get_user_identifier(m) for m in mentions if not m == user) + " "
)
@register.filter(name="status_preview_name")
def get_status_preview_name(obj):
"""text snippet with book context for a status"""
name = obj.__class__.__name__.lower()
if name == "review":
return "%s of <em>%s</em>" % (name, obj.book.title)
if name == "comment":
return "%s on <em>%s</em>" % (name, obj.book.title)
if name == "quotation":
return "%s from <em>%s</em>" % (name, obj.book.title)
return name
@register.filter(name="next_shelf") @register.filter(name="next_shelf")
def get_next_shelf(current_shelf): def get_next_shelf(current_shelf):
"""shelf you'd use to update reading progress""" """shelf you'd use to update reading progress"""
@ -168,17 +54,6 @@ def get_next_shelf(current_shelf):
return "to-read" return "to-read"
@register.filter(name="title")
def get_title(book):
"""display the subtitle if the title is short"""
if not book:
return ""
title = book.title
if len(title) < 6 and book.subtitle:
title = "{:s}: {:s}".format(title, book.subtitle)
return title
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def related_status(notification): def related_status(notification):
"""for notifications""" """for notifications"""
@ -212,31 +87,6 @@ def latest_read_through(book, user):
) )
@register.simple_tag(takes_context=False)
def active_read_through(book, user):
"""the most recent read activity"""
return (
models.ReadThrough.objects.filter(
user=user, book=book, finish_date__isnull=True
)
.order_by("-start_date")
.first()
)
@register.simple_tag(takes_context=False)
def comparison_bool(str1, str2):
"""idk why I need to write a tag for this, it reutrns a bool"""
return str1 == str2
@register.simple_tag(takes_context=False)
def get_lang():
"""get current language, strip to the first two letters"""
language = utils.translation.get_language()
return language[0 : language.find("-")]
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def mutuals_count(context, user): def mutuals_count(context, user):
"""how many users that you follow, follow them""" """how many users that you follow, follow them"""

View file

@ -0,0 +1,22 @@
""" template filters for status interaction buttons """
from django import template
from bookwyrm import models
register = template.Library()
@register.filter(name="liked")
def get_user_liked(user, status):
"""did the given user fav a status?"""
try:
models.Favorite.objects.get(user=user, status=status)
return True
except models.Favorite.DoesNotExist:
return False
@register.filter(name="boosted")
def get_user_boosted(user, status):
"""did the given user fav a status?"""
return user.id in status.boosters.all().values_list("user", flat=True)

View file

@ -0,0 +1,12 @@
""" template filters used for creating the layout"""
from django import template, utils
register = template.Library()
@register.simple_tag(takes_context=False)
def get_lang():
"""get current language, strip to the first two letters"""
language = utils.translation.get_language()
return language[0 : language.find("-")]

View file

@ -0,0 +1,14 @@
""" template filters """
from django import template
from bookwyrm.views.status import to_markdown
register = template.Library()
@register.filter(name="to_markdown")
def get_markdown(content):
"""convert markdown to html"""
if content:
return to_markdown(content)
return None

View file

@ -0,0 +1,59 @@
""" template filters """
from dateutil.relativedelta import relativedelta
from django import template
from django.contrib.humanize.templatetags.humanize import naturaltime, naturalday
from django.utils import timezone
from bookwyrm import models
from bookwyrm.templatetags.utilities import get_user_identifier
register = template.Library()
@register.filter(name="mentions")
def get_mentions(status, user):
"""people to @ in a reply: the parent and all mentions"""
mentions = set([status.user] + list(status.mention_users.all()))
return (
" ".join("@" + get_user_identifier(m) for m in mentions if not m == user) + " "
)
@register.filter(name="replies")
def get_replies(status):
"""get all direct replies to a status"""
# TODO: this limit could cause problems
return models.Status.objects.filter(
reply_parent=status,
deleted=False,
).select_subclasses()[:10]
@register.filter(name="parent")
def get_parent(status):
"""get the reply parent for a status"""
return (
models.Status.objects.filter(id=status.reply_parent_id)
.select_subclasses()
.get()
)
@register.filter(name="boosted_status")
def get_boosted(boost):
"""load a boosted status. have to do this or it won't get foreign keys"""
return models.Status.objects.select_subclasses().get(id=boost.boosted_status.id)
@register.filter(name="published_date")
def get_published_date(date):
"""less verbose combo of humanize filters"""
if not date:
return ""
now = timezone.now()
delta = relativedelta(now, date)
if delta.years:
return naturalday(date)
if delta.days:
return naturalday(date, "M j")
return naturaltime(date)

View file

@ -0,0 +1,35 @@
""" template filters for really common utilities """
from uuid import uuid4
from django import template
register = template.Library()
@register.filter(name="uuid")
def get_uuid(identifier):
"""for avoiding clashing ids when there are many forms"""
return "%s%s" % (identifier, uuid4())
@register.filter(name="username")
def get_user_identifier(user):
"""use localname for local users, username for remote"""
return user.localname if user.localname else user.username
@register.filter(name="title")
def get_title(book):
"""display the subtitle if the title is short"""
if not book:
return ""
title = book.title
if len(title) < 6 and book.subtitle:
title = "{:s}: {:s}".format(title, book.subtitle)
return title
@register.simple_tag(takes_context=False)
def comparison_bool(str1, str2):
"""idk why I need to write a tag for this, it reutrns a bool"""
return str1 == str2

View file

@ -84,13 +84,6 @@ class AbstractConnector(TestCase):
"""barebones connector for search with defaults""" """barebones connector for search with defaults"""
self.assertIsInstance(self.connector.book_mappings, list) self.assertIsInstance(self.connector.book_mappings, list)
def test_is_available(self):
"""this isn't used...."""
self.assertTrue(self.connector.is_available())
self.connector.max_query_count = 1
self.connector.connector.query_count = 2
self.assertFalse(self.connector.is_available())
def test_get_or_create_book_existing(self): def test_get_or_create_book_existing(self):
"""find an existing book by remote/origin id""" """find an existing book by remote/origin id"""
self.assertEqual(models.Book.objects.count(), 1) self.assertEqual(models.Book.objects.count(), 1)

View file

@ -53,7 +53,6 @@ class AbstractConnector(TestCase):
self.assertEqual(connector.isbn_search_url, "https://example.com/isbn?q=") self.assertEqual(connector.isbn_search_url, "https://example.com/isbn?q=")
self.assertIsNone(connector.name) self.assertIsNone(connector.name)
self.assertEqual(connector.identifier, "example.com") self.assertEqual(connector.identifier, "example.com")
self.assertIsNone(connector.max_query_count)
self.assertFalse(connector.local) self.assertFalse(connector.local)
@responses.activate @responses.activate

View file

@ -64,7 +64,7 @@ class Inventaire(TestCase):
) )
self.assertEqual( self.assertEqual(
formatted.cover, formatted.cover,
"https://covers.inventaire.io/img/entities/ddb32e115a28dcc0465023869ba19f6868ec4042", "https://covers.inventaire.io/img/entities/ddb32",
) )
def test_get_cover_url(self): def test_get_cover_url(self):
@ -74,18 +74,18 @@ class Inventaire(TestCase):
self.assertEqual(result, "https://covers.inventaire.io/img/entities/d46a8") self.assertEqual(result, "https://covers.inventaire.io/img/entities/d46a8")
cover_blob = { cover_blob = {
"url": "https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000", "url": "https://commons.wikimedia.org/wiki/d.jpg?width=1000",
"file": "The Moonstone 1st ed.jpg", "file": "The Moonstone 1st ed.jpg",
"credits": { "credits": {
"text": "Wikimedia Commons", "text": "Wikimedia Commons",
"url": "https://commons.wikimedia.org/wiki/File:The Moonstone 1st ed.jpg", "url": "https://commons.wikimedia.org/wiki/File:The Moonstone.jpg",
}, },
} }
result = self.connector.get_cover_url(cover_blob) result = self.connector.get_cover_url(cover_blob)
self.assertEqual( self.assertEqual(
result, result,
"https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000", "https://commons.wikimedia.org/wiki/d.jpg?width=1000",
) )
@responses.activate @responses.activate

View file

@ -7,7 +7,7 @@
"label": "The Stories of Vladimir Nabokov", "label": "The Stories of Vladimir Nabokov",
"description": "book by Vladimir Nabokov", "description": "book by Vladimir Nabokov",
"image": [ "image": [
"ddb32e115a28dcc0465023869ba19f6868ec4042" "ddb32"
], ],
"_score": 25.180836, "_score": 25.180836,
"_popularity": 4 "_popularity": 4

View file

@ -2,12 +2,17 @@
import re import re
from unittest.mock import patch from unittest.mock import patch
from dateutil.relativedelta import relativedelta
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from bookwyrm import models from bookwyrm import models
from bookwyrm.templatetags import bookwyrm_tags from bookwyrm.templatetags import (
bookwyrm_tags,
interaction,
markdown,
status_display,
utilities,
)
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
@ -33,12 +38,6 @@ class TemplateTags(TestCase):
) )
self.book = models.Edition.objects.create(title="Test Book") self.book = models.Edition.objects.create(title="Test Book")
def test_dict_key(self, _):
"""just getting a value out of a dict"""
test_dict = {"a": 1, "b": 3}
self.assertEqual(bookwyrm_tags.dict_key(test_dict, "a"), 1)
self.assertEqual(bookwyrm_tags.dict_key(test_dict, "c"), 0)
def test_get_user_rating(self, _): def test_get_user_rating(self, _):
"""get a user's most recent rating of a book""" """get a user's most recent rating of a book"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -52,27 +51,14 @@ class TemplateTags(TestCase):
def test_get_user_identifer_local(self, _): def test_get_user_identifer_local(self, _):
"""fall back to the simplest uid available""" """fall back to the simplest uid available"""
self.assertNotEqual(self.user.username, self.user.localname) self.assertNotEqual(self.user.username, self.user.localname)
self.assertEqual(bookwyrm_tags.get_user_identifier(self.user), "mouse") self.assertEqual(utilities.get_user_identifier(self.user), "mouse")
def test_get_user_identifer_remote(self, _): def test_get_user_identifer_remote(self, _):
"""for a remote user, should be their full username""" """for a remote user, should be their full username"""
self.assertEqual( self.assertEqual(
bookwyrm_tags.get_user_identifier(self.remote_user), "rat@example.com" utilities.get_user_identifier(self.remote_user), "rat@example.com"
) )
def test_get_notification_count(self, _):
"""just countin'"""
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0)
models.Notification.objects.create(user=self.user, notification_type="FAVORITE")
models.Notification.objects.create(user=self.user, notification_type="MENTION")
models.Notification.objects.create(
user=self.remote_user, notification_type="FOLLOW"
)
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 2)
def test_get_replies(self, _): def test_get_replies(self, _):
"""direct replies to a status""" """direct replies to a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -95,7 +81,7 @@ class TemplateTags(TestCase):
deleted_date=timezone.now(), deleted_date=timezone.now(),
) )
replies = bookwyrm_tags.get_replies(parent) replies = status_display.get_replies(parent)
self.assertEqual(len(replies), 2) self.assertEqual(len(replies), 2)
self.assertTrue(first_child in replies) self.assertTrue(first_child in replies)
self.assertTrue(second_child in replies) self.assertTrue(second_child in replies)
@ -111,7 +97,7 @@ class TemplateTags(TestCase):
reply_parent=parent, user=self.user, content="hi" reply_parent=parent, user=self.user, content="hi"
) )
result = bookwyrm_tags.get_parent(child) result = status_display.get_parent(child)
self.assertEqual(result, parent) self.assertEqual(result, parent)
self.assertIsInstance(result, models.Review) self.assertIsInstance(result, models.Review)
@ -119,44 +105,26 @@ class TemplateTags(TestCase):
"""did a user like a status""" """did a user like a status"""
status = models.Review.objects.create(user=self.remote_user, book=self.book) status = models.Review.objects.create(user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status)) self.assertFalse(interaction.get_user_liked(self.user, status))
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.Favorite.objects.create(user=self.user, status=status) models.Favorite.objects.create(user=self.user, status=status)
self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status)) self.assertTrue(interaction.get_user_liked(self.user, status))
def test_get_user_boosted(self, _): def test_get_user_boosted(self, _):
"""did a user boost a status""" """did a user boost a status"""
status = models.Review.objects.create(user=self.remote_user, book=self.book) status = models.Review.objects.create(user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status)) self.assertFalse(interaction.get_user_boosted(self.user, status))
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.Boost.objects.create(user=self.user, boosted_status=status) models.Boost.objects.create(user=self.user, boosted_status=status)
self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status)) self.assertTrue(interaction.get_user_boosted(self.user, status))
def test_follow_request_exists(self, _):
"""does a user want to follow"""
self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user)
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.UserFollowRequest.objects.create(
user_subject=self.user, user_object=self.remote_user
)
self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user)
)
self.assertTrue(
bookwyrm_tags.follow_request_exists(self.remote_user, self.user)
)
def test_get_boosted(self, _): def test_get_boosted(self, _):
"""load a boosted status""" """load a boosted status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Review.objects.create(user=self.remote_user, book=self.book) status = models.Review.objects.create(user=self.remote_user, book=self.book)
boost = models.Boost.objects.create(user=self.user, boosted_status=status) boost = models.Boost.objects.create(user=self.user, boosted_status=status)
boosted = bookwyrm_tags.get_boosted(boost) boosted = status_display.get_boosted(boost)
self.assertIsInstance(boosted, models.Review) self.assertIsInstance(boosted, models.Review)
self.assertEqual(boosted, status) self.assertEqual(boosted, status)
@ -178,48 +146,23 @@ class TemplateTags(TestCase):
def test_get_uuid(self, _): def test_get_uuid(self, _):
"""uuid functionality""" """uuid functionality"""
uuid = bookwyrm_tags.get_uuid("hi") uuid = utilities.get_uuid("hi")
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid)) self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
def test_get_markdown(self, _): def test_get_markdown(self, _):
"""mardown format data""" """mardown format data"""
result = bookwyrm_tags.get_markdown("_hi_") result = markdown.get_markdown("_hi_")
self.assertEqual(result, "<p><em>hi</em></p>") self.assertEqual(result, "<p><em>hi</em></p>")
result = bookwyrm_tags.get_markdown("<marquee>_hi_</marquee>") result = markdown.get_markdown("<marquee>_hi_</marquee>")
self.assertEqual(result, "<p><em>hi</em></p>") self.assertEqual(result, "<p><em>hi</em></p>")
def test_get_mentions(self, _): def test_get_mentions(self, _):
"""list of people mentioned""" """list of people mentioned"""
status = models.Status.objects.create(content="hi", user=self.remote_user) status = models.Status.objects.create(content="hi", user=self.remote_user)
result = bookwyrm_tags.get_mentions(status, self.user) result = status_display.get_mentions(status, self.user)
self.assertEqual(result, "@rat@example.com ") self.assertEqual(result, "@rat@example.com ")
def test_get_status_preview_name(self, _):
"""status context string"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.user)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, "status")
status = models.Review.objects.create(
content="hi", user=self.user, book=self.book
)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, "review of <em>Test Book</em>")
status = models.Comment.objects.create(
content="hi", user=self.user, book=self.book
)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, "comment on <em>Test Book</em>")
status = models.Quotation.objects.create(
content="hi", user=self.user, book=self.book
)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, "quotation from <em>Test Book</em>")
def test_related_status(self, _): def test_related_status(self, _):
"""gets the subclass model for a notification status""" """gets the subclass model for a notification status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):

View file

@ -59,7 +59,6 @@ class BookViews(TestCase):
result.render() result.render()
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
request = self.factory.get("")
with patch("bookwyrm.views.books.is_api_request") as is_api: with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = True is_api.return_value = True
result = view(request, self.book.id) result = view(request, self.book.id)

View file

@ -60,7 +60,12 @@ class FederationViews(TestCase):
def test_server_page_block(self): def test_server_page_block(self):
"""block a server""" """block a server"""
server = models.FederatedServer.objects.create(server_name="hi.there.com") server = models.FederatedServer.objects.create(
server_name="hi.there.com", application_type="bookwyrm"
)
connector = models.Connector.objects.get(
identifier="hi.there.com",
)
self.remote_user.federated_server = server self.remote_user.federated_server = server
self.remote_user.save() self.remote_user.save()
@ -72,17 +77,32 @@ class FederationViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
view(request, server.id) view(request, server.id)
server.refresh_from_db() server.refresh_from_db()
self.remote_user.refresh_from_db() self.remote_user.refresh_from_db()
self.assertEqual(server.status, "blocked") self.assertEqual(server.status, "blocked")
# and the user was deactivated # and the user was deactivated
self.assertFalse(self.remote_user.is_active) self.assertFalse(self.remote_user.is_active)
self.assertEqual(self.remote_user.deactivation_reason, "domain_block")
# and the connector was disabled
connector.refresh_from_db()
self.assertFalse(connector.active)
self.assertEqual(connector.deactivation_reason, "domain_block")
def test_server_page_unblock(self): def test_server_page_unblock(self):
"""unblock a server""" """unblock a server"""
server = models.FederatedServer.objects.create( server = models.FederatedServer.objects.create(
server_name="hi.there.com", status="blocked" server_name="hi.there.com", status="blocked", application_type="bookwyrm"
) )
connector = models.Connector.objects.get(
identifier="hi.there.com",
)
connector.active = False
connector.deactivation_reason = "domain_block"
connector.save()
self.remote_user.federated_server = server self.remote_user.federated_server = server
self.remote_user.is_active = False self.remote_user.is_active = False
self.remote_user.deactivation_reason = "domain_block" self.remote_user.deactivation_reason = "domain_block"
@ -96,8 +116,15 @@ class FederationViews(TestCase):
server.refresh_from_db() server.refresh_from_db()
self.remote_user.refresh_from_db() self.remote_user.refresh_from_db()
self.assertEqual(server.status, "federated") self.assertEqual(server.status, "federated")
# and the user was re-activated # and the user was re-activated
self.assertTrue(self.remote_user.is_active) self.assertTrue(self.remote_user.is_active)
self.assertIsNone(self.remote_user.deactivation_reason)
# and the connector was re-enabled
connector.refresh_from_db()
self.assertTrue(connector.active)
self.assertIsNone(connector.deactivation_reason)
def test_add_view_get(self): def test_add_view_get(self):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""

View file

@ -122,6 +122,14 @@ class ListViews(TestCase):
view = views.List.as_view() view = views.List.as_view()
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
with patch("bookwyrm.views.list.is_api_request") as is_api: with patch("bookwyrm.views.list.is_api_request") as is_api:
is_api.return_value = False is_api.return_value = False
@ -130,6 +138,81 @@ class ListViews(TestCase):
result.render() result.render()
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_list_page_sorted(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.List.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
for (i, book) in enumerate([self.book, self.book_two, self.book_three]):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=book,
approved=True,
order=i + 1,
)
request = self.factory.get("/?sort_by=order")
request.user = self.local_user
with patch("bookwyrm.views.list.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.list.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
request = self.factory.get("/?sort_by=title")
request.user = self.local_user
with patch("bookwyrm.views.list.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.list.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
request = self.factory.get("/?sort_by=rating")
request.user = self.local_user
with patch("bookwyrm.views.list.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.list.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
request = self.factory.get("/?sort_by=sdkfh")
request.user = self.local_user
with patch("bookwyrm.views.list.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.list.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_list_page_empty(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.List.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.list.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.list.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_list_page_logged_out(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.List.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
request = self.factory.get("")
request.user = self.anonymous_user request.user = self.anonymous_user
with patch("bookwyrm.views.list.is_api_request") as is_api: with patch("bookwyrm.views.list.is_api_request") as is_api:
is_api.return_value = False is_api.return_value = False
@ -138,12 +221,32 @@ class ListViews(TestCase):
result.render() result.render()
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_list_page_json_view(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.List.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
with patch("bookwyrm.views.list.is_api_request") as is_api: with patch("bookwyrm.views.list.is_api_request") as is_api:
is_api.return_value = True is_api.return_value = True
result = view(request, self.list.id) result = view(request, self.list.id)
self.assertIsInstance(result, ActivitypubResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_list_page_json_view_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.List.as_view()
request = self.factory.get("")
request.user = self.local_user
request = self.factory.get("/?page=1") request = self.factory.get("/?page=1")
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.views.list.is_api_request") as is_api: with patch("bookwyrm.views.list.is_api_request") as is_api:
@ -204,466 +307,34 @@ class ListViews(TestCase):
result = view(request, self.list.id) result = view(request, self.list.id)
self.assertEqual(result.status_code, 302) self.assertEqual(result.status_code, 302)
def test_curate_approve(self): def test_user_lists_page(self):
"""approve a pending item""" """there are so many views, this just makes sure it LOADS"""
view = views.Curate.as_view() view = views.UserLists.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
pending = models.ListItem.objects.create( models.List.objects.create(name="Public list", user=self.local_user)
book_list=self.list, models.List.objects.create(
user=self.local_user, name="Private list", privacy="direct", user=self.local_user
book=self.book,
approved=False,
order=1,
) )
request = self.factory.get("")
request = self.factory.post(
"",
{
"item": pending.id,
"approved": "true",
},
)
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: result = view(request, self.local_user.localname)
view(request, self.list.id) self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(mock.call_count, 2) def test_user_lists_page_logged_out(self):
activity = json.loads(mock.call_args[0][1]) """there are so many views, this just makes sure it LOADS"""
self.assertEqual(activity["type"], "Add") view = views.UserLists.as_view()
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
pending.refresh_from_db()
self.assertEqual(self.list.books.count(), 1)
self.assertEqual(self.list.listitem_set.first(), pending)
self.assertTrue(pending.approved)
def test_curate_reject(self):
"""approve a pending item"""
view = views.Curate.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
pending = models.ListItem.objects.create( models.List.objects.create(name="Public list", user=self.local_user)
book_list=self.list, models.List.objects.create(
user=self.local_user, name="Private list", privacy="direct", user=self.local_user
book=self.book,
approved=False,
order=1,
) )
request = self.factory.get("")
request.user = self.anonymous_user
request = self.factory.post( result = view(request, self.local_user.username)
"", self.assertIsInstance(result, TemplateResponse)
{ result.render()
"item": pending.id, self.assertEqual(result.status_code, 200)
"approved": "false",
},
)
request.user = self.local_user
view(request, self.list.id)
self.assertFalse(self.list.books.exists())
self.assertFalse(models.ListItem.objects.exists())
def test_add_book(self):
"""put a book on a list"""
request = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.list.add_book(request)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.local_user)
self.assertTrue(item.approved)
def test_add_two_books(self):
"""
Putting two books on the list. The first should have an order value of
1 and the second should have an order value of 2.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
def test_add_three_books_and_remove_second(self):
"""
Put three books on a list and then remove the one in the middle. The
ordering of the list should adjust to not have a gap.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
request_three = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request_three.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
views.list.add_book(request_three)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[2].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
remove_request = self.factory.post("", {"item": items[1].id})
remove_request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.remove_book(remove_request, self.list.id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
def test_adding_book_with_a_pending_book(self):
"""
When a list contains any pending books, the pending books should have
be at the end of the list by order. If a book is added while a book is
pending, its order should precede the pending books.
"""
request = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_two,
approved=False,
order=2,
)
views.list.add_book(request)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[0].order, 1)
self.assertTrue(items[0].approved)
self.assertEqual(items[1].book, self.book_three)
self.assertEqual(items[1].order, 2)
self.assertTrue(items[1].approved)
self.assertEqual(items[2].book, self.book_two)
self.assertEqual(items[2].order, 3)
self.assertFalse(items[2].approved)
def test_approving_one_pending_book_from_multiple(self):
"""
When a list contains any pending books, the pending books should have
be at the end of the list by order. If a pending book is approved, then
its order should be at the end of the approved books and before the
remaining pending books.
"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book_two,
approved=True,
order=2,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_three,
approved=False,
order=3,
)
to_be_approved = models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_four,
approved=False,
order=4,
)
view = views.Curate.as_view()
request = self.factory.post(
"",
{
"item": to_be_approved.id,
"approved": "true",
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
view(request, self.list.id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[0].order, 1)
self.assertTrue(items[0].approved)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[1].order, 2)
self.assertTrue(items[1].approved)
self.assertEqual(items[2].book, self.book_four)
self.assertEqual(items[2].order, 3)
self.assertTrue(items[2].approved)
self.assertEqual(items[3].book, self.book_three)
self.assertEqual(items[3].order, 4)
self.assertFalse(items[3].approved)
def test_add_three_books_and_move_last_to_first(self):
"""
Put three books on the list and move the last book to the first
position.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
request_three = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request_three.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
views.list.add_book(request_three)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[2].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
set_position_request = self.factory.post("", {"position": 1})
set_position_request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.set_book_position(set_position_request, items[2].id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book_three)
self.assertEqual(items[1].book, self.book)
self.assertEqual(items[2].book, self.book_two)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
def test_add_book_outsider(self):
"""put a book on a list"""
self.list.curation = "open"
self.list.save(broadcast=False)
request = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request.user = self.rat
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.list.add_book(request)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.rat.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.rat)
self.assertTrue(item.approved)
def test_add_book_pending(self):
"""put a book on a list awaiting approval"""
self.list.curation = "curated"
self.list.save(broadcast=False)
request = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request.user = self.rat
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.list.add_book(request)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.rat.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
item = self.list.listitem_set.get()
self.assertEqual(activity["object"]["id"], item.remote_id)
self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.rat)
self.assertFalse(item.approved)
def test_add_book_self_curated(self):
"""put a book on a list automatically approved"""
self.list.curation = "curated"
self.list.save(broadcast=False)
request = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.list.add_book(request)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.local_user)
self.assertTrue(item.approved)
def test_remove_book(self):
"""take an item off a list"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
item = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
order=1,
)
self.assertTrue(self.list.listitem_set.exists())
request = self.factory.post(
"",
{
"item": item.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.remove_book(request, self.list.id)
self.assertFalse(self.list.listitem_set.exists())
def test_remove_book_unauthorized(self):
"""take an item off a list"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
item = models.ListItem.objects.create(
book_list=self.list, user=self.local_user, book=self.book, order=1
)
self.assertTrue(self.list.listitem_set.exists())
request = self.factory.post(
"",
{
"item": item.id,
},
)
request.user = self.rat
views.list.remove_book(request, self.list.id)
self.assertTrue(self.list.listitem_set.exists())

View file

@ -0,0 +1,529 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
# pylint: disable=unused-argument
class ListActionViews(TestCase):
"""tag views"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
remote_id="https://example.com/users/mouse",
)
self.rat = models.User.objects.create_user(
"rat@local.com",
"rat@rat.com",
"ratword",
local=True,
localname="rat",
remote_id="https://example.com/users/rat",
)
work = models.Work.objects.create(title="Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=work,
)
work_two = models.Work.objects.create(title="Labori")
self.book_two = models.Edition.objects.create(
title="Example Edition 2",
remote_id="https://example.com/book/2",
parent_work=work_two,
)
work_three = models.Work.objects.create(title="Trabajar")
self.book_three = models.Edition.objects.create(
title="Example Edition 3",
remote_id="https://example.com/book/3",
parent_work=work_three,
)
work_four = models.Work.objects.create(title="Travailler")
self.book_four = models.Edition.objects.create(
title="Example Edition 4",
remote_id="https://example.com/book/4",
parent_work=work_four,
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.list = models.List.objects.create(
name="Test List", user=self.local_user
)
self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False
models.SiteSettings.objects.create()
def test_curate_approve(self):
"""approve a pending item"""
view = views.Curate.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
pending = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=False,
order=1,
)
request = self.factory.post(
"",
{
"item": pending.id,
"approved": "true",
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
view(request, self.list.id)
self.assertEqual(mock.call_count, 2)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
pending.refresh_from_db()
self.assertEqual(self.list.books.count(), 1)
self.assertEqual(self.list.listitem_set.first(), pending)
self.assertTrue(pending.approved)
def test_curate_reject(self):
"""approve a pending item"""
view = views.Curate.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
pending = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=False,
order=1,
)
request = self.factory.post(
"",
{
"item": pending.id,
"approved": "false",
},
)
request.user = self.local_user
view(request, self.list.id)
self.assertFalse(self.list.books.exists())
self.assertFalse(models.ListItem.objects.exists())
def test_add_book(self):
"""put a book on a list"""
request = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.list.add_book(request)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.local_user)
self.assertTrue(item.approved)
def test_add_two_books(self):
"""
Putting two books on the list. The first should have an order value of
1 and the second should have an order value of 2.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
def test_add_three_books_and_remove_second(self):
"""
Put three books on a list and then remove the one in the middle. The
ordering of the list should adjust to not have a gap.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
request_three = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request_three.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
views.list.add_book(request_three)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[2].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
remove_request = self.factory.post("", {"item": items[1].id})
remove_request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.remove_book(remove_request, self.list.id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
def test_adding_book_with_a_pending_book(self):
"""
When a list contains any pending books, the pending books should have
be at the end of the list by order. If a book is added while a book is
pending, its order should precede the pending books.
"""
request = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_two,
approved=False,
order=2,
)
views.list.add_book(request)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[0].order, 1)
self.assertTrue(items[0].approved)
self.assertEqual(items[1].book, self.book_three)
self.assertEqual(items[1].order, 2)
self.assertTrue(items[1].approved)
self.assertEqual(items[2].book, self.book_two)
self.assertEqual(items[2].order, 3)
self.assertFalse(items[2].approved)
def test_approving_one_pending_book_from_multiple(self):
"""
When a list contains any pending books, the pending books should have
be at the end of the list by order. If a pending book is approved, then
its order should be at the end of the approved books and before the
remaining pending books.
"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book_two,
approved=True,
order=2,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_three,
approved=False,
order=3,
)
to_be_approved = models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_four,
approved=False,
order=4,
)
view = views.Curate.as_view()
request = self.factory.post(
"",
{
"item": to_be_approved.id,
"approved": "true",
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
view(request, self.list.id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[0].order, 1)
self.assertTrue(items[0].approved)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[1].order, 2)
self.assertTrue(items[1].approved)
self.assertEqual(items[2].book, self.book_four)
self.assertEqual(items[2].order, 3)
self.assertTrue(items[2].approved)
self.assertEqual(items[3].book, self.book_three)
self.assertEqual(items[3].order, 4)
self.assertFalse(items[3].approved)
def test_add_three_books_and_move_last_to_first(self):
"""
Put three books on the list and move the last book to the first
position.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
request_three = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request_three.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
views.list.add_book(request_three)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[2].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
set_position_request = self.factory.post("", {"position": 1})
set_position_request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.set_book_position(set_position_request, items[2].id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book_three)
self.assertEqual(items[1].book, self.book)
self.assertEqual(items[2].book, self.book_two)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
def test_add_book_outsider(self):
"""put a book on a list"""
self.list.curation = "open"
self.list.save(broadcast=False)
request = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request.user = self.rat
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.list.add_book(request)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.rat.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.rat)
self.assertTrue(item.approved)
def test_add_book_pending(self):
"""put a book on a list awaiting approval"""
self.list.curation = "curated"
self.list.save(broadcast=False)
request = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request.user = self.rat
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.list.add_book(request)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.rat.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
item = self.list.listitem_set.get()
self.assertEqual(activity["object"]["id"], item.remote_id)
self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.rat)
self.assertFalse(item.approved)
def test_add_book_self_curated(self):
"""put a book on a list automatically approved"""
self.list.curation = "curated"
self.list.save(broadcast=False)
request = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.list.add_book(request)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["target"], self.list.remote_id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.local_user)
self.assertTrue(item.approved)
def test_remove_book(self):
"""take an item off a list"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
item = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
order=1,
)
self.assertTrue(self.list.listitem_set.exists())
request = self.factory.post(
"",
{
"item": item.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.remove_book(request, self.list.id)
self.assertFalse(self.list.listitem_set.exists())
def test_remove_book_unauthorized(self):
"""take an item off a list"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
item = models.ListItem.objects.create(
book_list=self.list, user=self.local_user, book=self.book, order=1
)
self.assertTrue(self.list.listitem_set.exists())
request = self.factory.post(
"",
{
"item": item.id,
},
)
request.user = self.rat
views.list.remove_book(request, self.list.id)
self.assertTrue(self.list.listitem_set.exists())

View file

@ -29,7 +29,7 @@ class Author(View):
"author": author, "author": author,
"books": [b.default_edition for b in books], "books": [b.default_edition for b in books],
} }
return TemplateResponse(request, "author.html", data) return TemplateResponse(request, "author/author.html", data)
@method_decorator(login_required, name="dispatch") @method_decorator(login_required, name="dispatch")
@ -43,7 +43,7 @@ class EditAuthor(View):
"""info about a book""" """info about a book"""
author = get_object_or_404(models.Author, id=author_id) author = get_object_or_404(models.Author, id=author_id)
data = {"author": author, "form": forms.AuthorForm(instance=author)} data = {"author": author, "form": forms.AuthorForm(instance=author)}
return TemplateResponse(request, "edit_author.html", data) return TemplateResponse(request, "author/edit_author.html", data)
def post(self, request, author_id): def post(self, request, author_id):
"""edit a author cool""" """edit a author cool"""
@ -52,7 +52,7 @@ class EditAuthor(View):
form = forms.AuthorForm(request.POST, request.FILES, instance=author) form = forms.AuthorForm(request.POST, request.FILES, instance=author)
if not form.is_valid(): if not form.is_valid():
data = {"author": author, "form": form} data = {"author": author, "form": form}
return TemplateResponse(request, "edit_author.html", data) return TemplateResponse(request, "author/edit_author.html", data)
author = form.save() author = form.save()
return redirect("/author/%s" % author.id) return redirect("/author/%s" % author.id)

View file

@ -30,6 +30,7 @@ class Book(View):
def get(self, request, book_id, user_statuses=False): def get(self, request, book_id, user_statuses=False):
"""info about a book""" """info about a book"""
user_statuses = user_statuses if request.user.is_authenticated else False
try: try:
book = models.Book.objects.select_subclasses().get(id=book_id) book = models.Book.objects.select_subclasses().get(id=book_id)
except models.Book.DoesNotExist: except models.Book.DoesNotExist:
@ -51,9 +52,9 @@ class Book(View):
) )
# the reviews to show # the reviews to show
if user_statuses and request.user.is_authenticated: if user_statuses:
if user_statuses == "review": if user_statuses == "review":
queryset = book.review_set queryset = book.review_set.select_subclasses()
elif user_statuses == "comment": elif user_statuses == "comment":
queryset = book.comment_set queryset = book.comment_set
else: else:
@ -67,7 +68,9 @@ class Book(View):
"book": book, "book": book,
"statuses": paginated.get_page(request.GET.get("page")), "statuses": paginated.get_page(request.GET.get("page")),
"review_count": reviews.count(), "review_count": reviews.count(),
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")), "ratings": reviews.filter(Q(content__isnull=True) | Q(content=""))
if not user_statuses
else None,
"rating": reviews.aggregate(Avg("rating"))["rating__avg"], "rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": privacy_filter( "lists": privacy_filter(
request.user, book.list_set.filter(listitem__approved=True) request.user, book.list_set.filter(listitem__approved=True)

View file

@ -5,7 +5,7 @@ from urllib.parse import urlencode
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.db.models import Avg, Count, Q, Max from django.db.models import Avg, Count, DecimalField, Q, Max
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
@ -108,31 +108,23 @@ class List(View):
if direction not in ("ascending", "descending"): if direction not in ("ascending", "descending"):
direction = "ascending" direction = "ascending"
internal_sort_by = { directional_sort_by = {
"order": "order", "order": "order",
"title": "book__title", "title": "book__title",
"rating": "average_rating", "rating": "average_rating",
} }[sort_by]
directional_sort_by = internal_sort_by[sort_by]
if direction == "descending": if direction == "descending":
directional_sort_by = "-" + directional_sort_by directional_sort_by = "-" + directional_sort_by
if sort_by == "order": items = book_list.listitem_set
items = book_list.listitem_set.filter(approved=True).order_by( if sort_by == "rating":
directional_sort_by items = items.annotate(
) average_rating=Avg(
elif sort_by == "title": Coalesce("book__review__rating", 0.0),
items = book_list.listitem_set.filter(approved=True).order_by( output_field=DecimalField(),
directional_sort_by
)
elif sort_by == "rating":
items = (
book_list.listitem_set.annotate(
average_rating=Avg(Coalesce("book__review__rating", 0))
) )
.filter(approved=True)
.order_by(directional_sort_by)
) )
items = items.filter(approved=True).order_by(directional_sort_by)
paginated = Paginator(items, PAGE_LENGTH) paginated = Paginator(items, PAGE_LENGTH)

View file

@ -2,6 +2,7 @@
from collections import namedtuple from collections import namedtuple
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Count, OuterRef, Subquery, F, Q
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
@ -37,30 +38,41 @@ class Shelf(View):
return HttpResponseNotFound() return HttpResponseNotFound()
if not shelf.visible_to_user(request.user): if not shelf.visible_to_user(request.user):
return HttpResponseNotFound() return HttpResponseNotFound()
books = shelf.books
# this is a constructed "all books" view, with a fake "shelf" obj # this is a constructed "all books" view, with a fake "shelf" obj
else: else:
FakeShelf = namedtuple( FakeShelf = namedtuple(
"Shelf", ("identifier", "name", "user", "books", "privacy") "Shelf", ("identifier", "name", "user", "books", "privacy")
) )
books = models.Edition.objects.filter( books = models.Edition.objects.filter(
# privacy is ensured because the shelves are already filtered above
shelfbook__shelf__in=shelves.all() shelfbook__shelf__in=shelves.all()
).distinct() ).distinct()
shelf = FakeShelf("all", _("All books"), user, books, "public") shelf = FakeShelf("all", _("All books"), user, books, "public")
is_self = request.user == user
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(shelf.to_activity(**request.GET)) return ActivitypubResponse(shelf.to_activity(**request.GET))
reviews = privacy_filter(
request.user,
models.Review.objects.filter(
user=user,
rating__isnull=False,
book__id=OuterRef("id"),
),
).order_by("-published_date")
books = books.annotate(rating=Subquery(reviews.values("rating")[:1]))
paginated = Paginator( paginated = Paginator(
shelf.books.order_by("-updated_date"), books.order_by("-updated_date"),
PAGE_LENGTH, PAGE_LENGTH,
) )
page = paginated.get_page(request.GET.get("page")) page = paginated.get_page(request.GET.get("page"))
data = { data = {
"user": user, "user": user,
"is_self": is_self, "is_self": request.user == user,
"shelves": shelves.all(), "shelves": shelves.all(),
"shelf": shelf, "shelf": shelf,
"books": page, "books": page,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff