mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-10-31 22:19:00 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
1e71cf980c
93 changed files with 3000 additions and 2562 deletions
|
@ -30,7 +30,6 @@ class AbstractMinimalConnector(ABC):
|
|||
"covers_url",
|
||||
"search_url",
|
||||
"isbn_search_url",
|
||||
"max_query_count",
|
||||
"name",
|
||||
"identifier",
|
||||
"local",
|
||||
|
@ -102,13 +101,6 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
# title we handle separately.
|
||||
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):
|
||||
"""translate arbitrary json into an Activitypub dataclass"""
|
||||
# first, check if we have the origin_id saved
|
||||
|
|
|
@ -87,7 +87,7 @@ def first_search_result(query, min_confidence=0.1):
|
|||
|
||||
def get_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)
|
||||
|
||||
|
||||
|
|
48
bookwyrm/migrations/0074_auto_20210511_1829.py
Normal file
48
bookwyrm/migrations/0074_auto_20210511_1829.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -6,6 +6,16 @@ from bookwyrm.settings import DOMAIN
|
|||
from .fields import RemoteIdField
|
||||
|
||||
|
||||
DeactivationReason = models.TextChoices(
|
||||
"DeactivationReason",
|
||||
[
|
||||
"self_deletion",
|
||||
"moderator_deletion",
|
||||
"domain_block",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class BookWyrmModel(models.Model):
|
||||
"""shared fields"""
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from django.db import models
|
||||
from bookwyrm.connectors.settings import CONNECTORS
|
||||
|
||||
from .base_model import BookWyrmModel
|
||||
from .base_model import BookWyrmModel, DeactivationReason
|
||||
|
||||
|
||||
ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
|
||||
|
@ -17,6 +17,10 @@ class Connector(BookWyrmModel):
|
|||
local = models.BooleanField(default=False)
|
||||
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
return "{} ({})".format(
|
||||
self.identifier,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" connections to external ActivityPub servers """
|
||||
from urllib.parse import urlparse
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from .base_model import BookWyrmModel
|
||||
|
||||
|
@ -34,6 +35,13 @@ class FederatedServer(BookWyrmModel):
|
|||
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):
|
||||
"""unblock a server"""
|
||||
self.status = "federated"
|
||||
|
@ -43,6 +51,15 @@ class FederatedServer(BookWyrmModel):
|
|||
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
|
||||
def is_blocked(cls, url):
|
||||
"""look up if a domain is blocked"""
|
||||
|
|
|
@ -19,21 +19,11 @@ from bookwyrm.signatures import create_key_pair
|
|||
from bookwyrm.tasks import app
|
||||
from bookwyrm.utils import regex
|
||||
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from .base_model import BookWyrmModel, DeactivationReason
|
||||
from .federated_server import FederatedServer
|
||||
from . import fields, Review
|
||||
|
||||
|
||||
DeactivationReason = models.TextChoices(
|
||||
"DeactivationReason",
|
||||
[
|
||||
"self_deletion",
|
||||
"moderator_deletion",
|
||||
"domain_block",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class User(OrderedCollectionPageMixin, AbstractUser):
|
||||
"""a user who wants to read books"""
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
83
bookwyrm/templates/author/author.html
Normal file
83
bookwyrm/templates/author/author.html
Normal 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 %}
|
|
@ -29,44 +29,64 @@
|
|||
<div class="columns">
|
||||
<div class="column">
|
||||
<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 %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% 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 %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% 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 %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% 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 %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% 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 %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="column">
|
||||
<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 %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% 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 %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% 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 %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
{% endfor %}
|
|
@ -38,9 +38,8 @@
|
|||
{% if user_authenticated and can_edit_book %}
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ book.id }}/edit">
|
||||
<span class="icon icon-pencil" title="{% trans "Edit Book" %}">
|
||||
<span class="is-sr-only">{% trans "Edit Book" %}</span>
|
||||
</span>
|
||||
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
|
||||
<span>{% trans "Edit Book" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -163,12 +162,9 @@
|
|||
</div>
|
||||
<div class="column is-narrow">
|
||||
{% 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>
|
||||
</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">
|
||||
<form name="add-readthrough" action="/create-readthrough" method="post">
|
||||
{% include 'snippets/readthrough_form.html' with readthrough=None %}
|
||||
|
@ -183,6 +179,9 @@
|
|||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% if not readthroughs.exists %}
|
||||
<p>{% trans "You don't have any reading activity for this book." %}</p>
|
||||
{% endif %}
|
||||
{% for readthrough in readthroughs %}
|
||||
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
|
||||
{% endfor %}
|
||||
|
@ -195,7 +194,7 @@
|
|||
{% endif %}
|
||||
<div class="block" id="reviews">
|
||||
{% 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">
|
||||
<ul>
|
||||
{% url 'book' book.id as tab_url %}
|
||||
|
@ -225,46 +224,26 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% for review in statuses %}
|
||||
{% for status in statuses %}
|
||||
<div
|
||||
class="block"
|
||||
{% if status.status_type == 'Review' or status.status_type == 'Rating' %}
|
||||
itemprop="review"
|
||||
itemscope
|
||||
itemtype="https://schema.org/Review"
|
||||
{% endif %}
|
||||
>
|
||||
{% with status=review hide_book=True depth=1 %}
|
||||
{% include 'snippets/status/status.html' %}
|
||||
{% endwith %}
|
||||
{% include 'snippets/status/status.html' with status=status hide_book=True depth=1 %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if ratings %}
|
||||
<div class="block is-flex is-flex-wrap-wrap">
|
||||
{% for rating in ratings %}
|
||||
{% with user=rating.user %}
|
||||
<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 %}
|
||||
{% include 'book/rating.html' with user=rating.user rating=rating %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="block">
|
||||
{% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
|
||||
</div>
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
<p class="mb-2">
|
||||
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
||||
{{ form.publishers }}
|
||||
<span class="help">{% trans "Separate multiple publishers with commas." %}</span>
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
</p>
|
||||
{% for error in form.publishers.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
|
@ -162,7 +162,7 @@
|
|||
{% endif %}
|
||||
<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 %}>
|
||||
<p class="help">Separate multiple author names with commas.</p>
|
||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% block title %}{% blocktrans with book_title=work.title %}Editions of {{ book_title }}{% endblocktrans %}{% endblock %}
|
||||
|
||||
|
|
22
bookwyrm/templates/book/rating.html
Normal file
22
bookwyrm/templates/book/rating.html
Normal 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>
|
|
@ -1,5 +1,5 @@
|
|||
{% spaceless %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% with 0|uuid as uuid %}
|
||||
<div
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{% trans "Compose status" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
{% load humanize %}
|
||||
|
||||
<div class="card is-stretchable">
|
||||
|
@ -19,7 +20,7 @@
|
|||
|
||||
<div>
|
||||
{% if user.summary %}
|
||||
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
|
||||
{{ user.summary|to_markdown|safe|truncatechars_html:40 }}
|
||||
{% else %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}{% trans "Welcome" %}{% endblock %}
|
||||
|
||||
|
@ -49,7 +49,7 @@
|
|||
{% else %}
|
||||
|
||||
<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 request_received %}
|
||||
|
@ -64,7 +64,7 @@
|
|||
<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">
|
||||
{% for error in request_form.email.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
<p class="help is-danger">{{ error|escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||
|
@ -80,7 +80,7 @@
|
|||
{% include 'user/user_preview.html' with user=request.user %}
|
||||
{% if request.user.summary %}
|
||||
<div class="box content">
|
||||
{{ request.user.summary | to_markdown | safe }}
|
||||
{{ request.user.summary|to_markdown|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if book %}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{% extends 'feed/feed_layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
<h1 class="title">
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% block title %}{% trans "Updates" %}{% endblock %}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% load humanize %}
|
||||
<div class="columns is-mobile scroll-x mb-0">
|
||||
{% for user in suggested_users %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load status_display %}
|
||||
<div class="block">
|
||||
|
||||
{% with depth=depth|add:1 %}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{% if is_self and goal %}
|
||||
<div class="column is-narrow">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% 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 }}">
|
||||
<label for="import-item-{{ item.id }}">
|
||||
Line {{ item.index }}:
|
||||
<strong>{{ item.data|dict_key:'Title' }}</strong> by
|
||||
{{ item.data|dict_key:'Author' }}
|
||||
<strong>{{ item.data.Title }}</strong> by
|
||||
{{ item.data.Author }}
|
||||
</label>
|
||||
<p>
|
||||
{{ item.fail_reason }}.
|
||||
|
@ -90,8 +89,8 @@
|
|||
<li class="pb-1">
|
||||
<p>
|
||||
Line {{ item.index }}:
|
||||
<strong>{{ item.data|dict_key:'Title' }}</strong> by
|
||||
{{ item.data|dict_key:'Author' }}
|
||||
<strong>{{ item.data.Title }}</strong> by
|
||||
{{ item.data.Author }}
|
||||
</p>
|
||||
<p>
|
||||
{{ item.fail_reason }}.
|
||||
|
@ -130,10 +129,10 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.data|dict_key:'Title' }}
|
||||
{{ item.data.Title }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.data|dict_key:'Author' }}
|
||||
{{ item.data.Author }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.book %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load layout %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{% get_lang %}">
|
||||
|
@ -214,7 +214,7 @@
|
|||
</p>
|
||||
{% endif %}
|
||||
<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>
|
||||
</div>
|
||||
{% if site.footer_item %}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
{% extends 'lists/list_layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block panel %}
|
||||
{% if request.user == list.user and pending_count %}
|
||||
<div class="block content">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -50,9 +51,9 @@
|
|||
<p>
|
||||
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
|
||||
</p>
|
||||
<p>
|
||||
<div>
|
||||
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
||||
</p>
|
||||
</div>
|
||||
{% include 'snippets/shelve_button/shelve_button.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
<div class="columns is-multiline">
|
||||
{% for list in lists %}
|
||||
<div class="column is-one-quarter">
|
||||
|
@ -22,7 +22,7 @@
|
|||
{% endwith %}
|
||||
|
||||
<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 %}
|
||||
{{ list.description|to_markdown|safe|truncatechars_html:30 }}
|
||||
{% else %}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% block title %}{{ list.name }}{% endblock %}
|
||||
|
||||
|
@ -16,7 +15,7 @@
|
|||
{% if request.user == list.user %}
|
||||
<div class="column is-narrow">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Lists" %}{% endblock %}
|
||||
|
@ -18,7 +18,7 @@
|
|||
{% if request.user.is_authenticated %}
|
||||
<div class="column is-narrow">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
|
|
@ -38,6 +38,9 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="box has-background-primary-light">
|
||||
{% if site.allow_registration %}
|
||||
<h2 class="title">{% trans "Create an Account" %}</h2>
|
||||
|
@ -50,15 +53,15 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="block">
|
||||
{% include 'snippets/about.html' %}
|
||||
<div class="block">
|
||||
<div class="box">
|
||||
{% include 'snippets/about.html' %}
|
||||
|
||||
<p class="block">
|
||||
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="block">
|
||||
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends 'settings/admin_layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% 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>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
{{ comment.created_date | naturaltime }}
|
||||
{{ comment.created_date|naturaltime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% trans "Notifications" %}{% endblock %}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{% with results|first as local_results %}
|
||||
<ul class="block">
|
||||
{% 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 %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
{% extends 'search/layout.html' %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{% extends 'settings/admin_layout.html' %}
|
||||
{% block title %}{{ server.server_name }}{% endblock %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}{{ server.server_name }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{{ server.server_name }}
|
||||
|
@ -14,60 +15,64 @@
|
|||
|
||||
{% block panel %}
|
||||
<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>
|
||||
<dl>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Software:" %}</dt>
|
||||
<dd>{{ server.application_type }}</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Version:" %}</dt>
|
||||
<dd>{{ server.application_version }}</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Status:" %}</dt>
|
||||
<dd>{{ server.status }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="box is-flex-grow-1 content">
|
||||
<dl>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Software:" %}</dt>
|
||||
<dd>{{ server.application_type }}</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Version:" %}</dt>
|
||||
<dd>{{ server.application_version }}</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Status:" %}</dt>
|
||||
<dd>{{ server.status }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</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>
|
||||
<dl>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Users:" %}</dt>
|
||||
<dd>
|
||||
{{ users.count }}
|
||||
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Reports:" %}</dt>
|
||||
<dd>
|
||||
{{ reports.count }}
|
||||
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Followed by us:" %}</dt>
|
||||
<dd>
|
||||
{{ followed_by_us.count }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Followed by them:" %}</dt>
|
||||
<dd>
|
||||
{{ followed_by_them.count }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Blocked by us:" %}</dt>
|
||||
<dd>
|
||||
{{ blocked_by_us.count }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="box is-flex-grow-1 content">
|
||||
<dl>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Users:" %}</dt>
|
||||
<dd>
|
||||
{{ users.count }}
|
||||
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Reports:" %}</dt>
|
||||
<dd>
|
||||
{{ reports.count }}
|
||||
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Followed by us:" %}</dt>
|
||||
<dd>
|
||||
{{ followed_by_us.count }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Followed by them:" %}</dt>
|
||||
<dd>
|
||||
{{ followed_by_them.count }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Blocked by us:" %}</dt>
|
||||
<dd>
|
||||
{{ blocked_by_us.count }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
@ -78,11 +83,11 @@
|
|||
</div>
|
||||
<div class="column is-narrow">
|
||||
{% 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>
|
||||
</header>
|
||||
{% 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 %}
|
||||
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
|
||||
{% block edit-button %}
|
||||
<a href="{% url 'settings-import-blocklist' %}">
|
||||
<span class="icon icon-plus" title="{% trans 'Add server' %}">
|
||||
<span class="is-sr-only">{% trans "Add server" %}</span>
|
||||
</span>
|
||||
<span class="icon icon-plus" title="{% trans 'Add server' %}" aria-hidden="True"></span>
|
||||
<span>{% trans "Add server" %}</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -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 }}">
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% spaceless %}
|
||||
|
||||
{% load bookwyrm_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<figure
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% 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 %}
|
||||
{% else %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load interaction %}
|
||||
{% load utilities %}
|
||||
{% load i18n %}
|
||||
|
||||
{% with status.id|uuid as uuid %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% load humanize %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% with status_type=request.GET.status_type %}
|
||||
<div class="tab-group">
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% load status_display %}
|
||||
|
||||
{% 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 }}">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load interaction %}
|
||||
{% load utilities %}
|
||||
{% load i18n %}
|
||||
|
||||
{% 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 }}">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
<span class="column is-narrow pb-0">
|
||||
{% 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 %}
|
||||
{% 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>
|
||||
</h2>
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% if request.user|follow_request_exists:user %}
|
||||
{% if request.user in user.follow_requests.all %}
|
||||
<div class="field is-grouped">
|
||||
<form action="/accept-follow-request/" method="POST">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% spaceless %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
<div class="
|
||||
field is-grouped
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
<div class="select {{ class }}">
|
||||
{% with 0|uuid as uuid %}
|
||||
{% if not no_label %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<label class="label" tabindex="0" id="add-readthrough-focus">
|
||||
{% 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" }}">
|
||||
</label>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% with 0|uuid as report_uuid %}
|
||||
|
||||
{% trans "Report" as button_text %}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
|
||||
{% with book.id|uuid as uuid %}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% load i18n %}
|
||||
|
||||
{% for shelf in shelves %}
|
||||
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
||||
{% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
{% load i18n %}
|
||||
|
||||
{% with status_type=status.status_type %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% spaceless %}
|
||||
|
||||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if not hide_book %}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{% extends 'components/card.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block card-header %}
|
||||
<div class="card-header-title has-background-white-ter is-block">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load status_display %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if not status.deleted %}
|
||||
{% if status.status_type == 'Announce' %}
|
||||
<a href="{{ status.user.local_path }}">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% load status_display %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
|
@ -29,7 +30,7 @@
|
|||
</span>
|
||||
|
||||
{% if status.status_type == 'GeneratedNote' %}
|
||||
{{ status.content | safe }}
|
||||
{{ status.content|safe }}
|
||||
{% elif status.status_type == 'Rating' %}
|
||||
{% trans "rated" %}
|
||||
{% elif status.status_type == 'Review' %}
|
||||
|
@ -91,7 +92,7 @@
|
|||
|
||||
</h3>
|
||||
<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 %}
|
||||
<span class="ml-1">
|
||||
{% if status.progress_mode == 'PG' %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'components/dropdown.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block dropdown-trigger %}
|
||||
<span class="icon icon-dots-three m-0-mobile"></span>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
{% load utilities %}
|
||||
{% load i18n %}
|
||||
|
||||
{% with 0|uuid as uuid %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'components/dropdown.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block dropdown-trigger %}
|
||||
<span class="icon icon-dots-three">
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}{{ user.display_name }}{% endblock %}
|
||||
|
||||
|
@ -23,7 +24,7 @@
|
|||
|
||||
{% if user.summary %}
|
||||
<div class="column box has-background-white-bis content">
|
||||
{{ user.summary | to_markdown | safe }}
|
||||
{{ user.summary|to_markdown|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
{% if is_self %}
|
||||
<div class="column is-narrow">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'user/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block tabs %}
|
||||
{% with user|username as username %}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% extends 'user/layout.html' %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
{% load humanize %}
|
||||
{% load i18n %}
|
||||
|
||||
|
@ -35,7 +36,7 @@
|
|||
{% if is_self %}
|
||||
<div class="column is-narrow">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -58,7 +59,7 @@
|
|||
{% if is_self and shelf.id %}
|
||||
<div class="column is-narrow">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -79,7 +80,9 @@
|
|||
<th>{% trans "Shelved" %}</th>
|
||||
<th>{% trans "Started" %}</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 %}
|
||||
<th aria-hidden="true"></th>
|
||||
{% endif %}
|
||||
|
@ -99,18 +102,18 @@
|
|||
{% include 'snippets/authors.html' %}
|
||||
</td>
|
||||
<td data-title="{% trans "Shelved" %}">
|
||||
{{ book.created_date | naturalday }}
|
||||
{{ book.created_date|naturalday }}
|
||||
</td>
|
||||
{% latest_read_through book user as read_through %}
|
||||
<td data-title="{% trans "Started" %}">
|
||||
{{ read_through.start_date | naturalday |default_if_none:""}}
|
||||
{{ read_through.start_date|naturalday|default_if_none:""}}
|
||||
</td>
|
||||
<td data-title="{% trans "Finished" %}">
|
||||
{{ read_through.finish_date | naturalday |default_if_none:""}}
|
||||
{{ read_through.finish_date|naturalday|default_if_none:""}}
|
||||
</td>
|
||||
{% if ratings %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if shelf.user == request.user %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'user/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{{ user.display_name }}{% endblock %}
|
||||
|
||||
|
@ -12,9 +12,8 @@
|
|||
{% if is_self %}
|
||||
<div class="column is-narrow">
|
||||
<a href="{% url 'prefs-profile' %}">
|
||||
<span class="icon icon-pencil" title="Edit profile">
|
||||
<span class="is-sr-only">{% trans "Edit profile" %}</span>
|
||||
</span>
|
||||
<span class="icon icon-pencil" title="Edit profile" aria-hidden="true"></span>
|
||||
<span>{% trans "Edit profile" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -59,8 +58,9 @@
|
|||
<div class="columns is-mobile">
|
||||
<h2 class="title column">{% trans "User Activity" %}</h2>
|
||||
<div class="column is-narrow">
|
||||
<a class="icon icon-rss" target="_blank" href="{{ user.local_path }}/rss">
|
||||
<span class="is-sr-only">{% trans "RSS feed" %}</span>
|
||||
<a target="_blank" href="{{ user.local_path }}/rss">
|
||||
<span class="icon icon-rss" aria-hidden="true"></span>
|
||||
<span>{% trans "RSS feed" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load utilities %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
<div class="media block">
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
{% extends 'settings/admin_layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{{ user.username }}{% endblock %}
|
||||
{% block header %}{{ user.username }}{% endblock %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load markdown %}
|
||||
<div class="block columns">
|
||||
<div class="column is-flex is-flex-direction-column">
|
||||
<h4 class="title is-4">{% trans "User details" %}</h4>
|
||||
|
@ -7,7 +7,7 @@
|
|||
{% include 'user/user_preview.html' with user=user %}
|
||||
{% if user.summary %}
|
||||
<div class="box content has-background-white-ter is-shadowless">
|
||||
{{ user.summary | to_markdown | safe }}
|
||||
{{ user.summary|to_markdown|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
""" template filters """
|
||||
from uuid import uuid4
|
||||
|
||||
from django import template, utils
|
||||
from django import template
|
||||
from django.db.models import Avg
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.views.status import to_markdown
|
||||
from bookwyrm.templatetags.utilities import get_user_identifier
|
||||
|
||||
|
||||
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")
|
||||
def get_rating(book, user):
|
||||
"""get the overall rating of a book"""
|
||||
|
@ -43,119 +36,12 @@ def get_user_rating(book, user):
|
|||
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")
|
||||
def get_book_description(book):
|
||||
"""use the work's text if the book doesn't have it"""
|
||||
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")
|
||||
def get_next_shelf(current_shelf):
|
||||
"""shelf you'd use to update reading progress"""
|
||||
|
@ -168,17 +54,6 @@ def get_next_shelf(current_shelf):
|
|||
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)
|
||||
def related_status(notification):
|
||||
"""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)
|
||||
def mutuals_count(context, user):
|
||||
"""how many users that you follow, follow them"""
|
||||
|
|
22
bookwyrm/templatetags/interaction.py
Normal file
22
bookwyrm/templatetags/interaction.py
Normal 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)
|
12
bookwyrm/templatetags/layout.py
Normal file
12
bookwyrm/templatetags/layout.py
Normal 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("-")]
|
14
bookwyrm/templatetags/markdown.py
Normal file
14
bookwyrm/templatetags/markdown.py
Normal 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
|
59
bookwyrm/templatetags/status_display.py
Normal file
59
bookwyrm/templatetags/status_display.py
Normal 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)
|
35
bookwyrm/templatetags/utilities.py
Normal file
35
bookwyrm/templatetags/utilities.py
Normal 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
|
|
@ -84,13 +84,6 @@ class AbstractConnector(TestCase):
|
|||
"""barebones connector for search with defaults"""
|
||||
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):
|
||||
"""find an existing book by remote/origin id"""
|
||||
self.assertEqual(models.Book.objects.count(), 1)
|
||||
|
|
|
@ -53,7 +53,6 @@ class AbstractConnector(TestCase):
|
|||
self.assertEqual(connector.isbn_search_url, "https://example.com/isbn?q=")
|
||||
self.assertIsNone(connector.name)
|
||||
self.assertEqual(connector.identifier, "example.com")
|
||||
self.assertIsNone(connector.max_query_count)
|
||||
self.assertFalse(connector.local)
|
||||
|
||||
@responses.activate
|
||||
|
|
|
@ -64,7 +64,7 @@ class Inventaire(TestCase):
|
|||
)
|
||||
self.assertEqual(
|
||||
formatted.cover,
|
||||
"https://covers.inventaire.io/img/entities/ddb32e115a28dcc0465023869ba19f6868ec4042",
|
||||
"https://covers.inventaire.io/img/entities/ddb32",
|
||||
)
|
||||
|
||||
def test_get_cover_url(self):
|
||||
|
@ -74,18 +74,18 @@ class Inventaire(TestCase):
|
|||
self.assertEqual(result, "https://covers.inventaire.io/img/entities/d46a8")
|
||||
|
||||
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",
|
||||
"credits": {
|
||||
"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)
|
||||
self.assertEqual(
|
||||
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
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"label": "The Stories of Vladimir Nabokov",
|
||||
"description": "book by Vladimir Nabokov",
|
||||
"image": [
|
||||
"ddb32e115a28dcc0465023869ba19f6868ec4042"
|
||||
"ddb32"
|
||||
],
|
||||
"_score": 25.180836,
|
||||
"_popularity": 4
|
||||
|
|
|
@ -2,12 +2,17 @@
|
|||
import re
|
||||
from unittest.mock import patch
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
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")
|
||||
|
@ -33,12 +38,6 @@ class TemplateTags(TestCase):
|
|||
)
|
||||
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, _):
|
||||
"""get a user's most recent rating of a book"""
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
|
@ -52,27 +51,14 @@ class TemplateTags(TestCase):
|
|||
def test_get_user_identifer_local(self, _):
|
||||
"""fall back to the simplest uid available"""
|
||||
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, _):
|
||||
"""for a remote user, should be their full username"""
|
||||
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, _):
|
||||
"""direct replies to a status"""
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
|
@ -95,7 +81,7 @@ class TemplateTags(TestCase):
|
|||
deleted_date=timezone.now(),
|
||||
)
|
||||
|
||||
replies = bookwyrm_tags.get_replies(parent)
|
||||
replies = status_display.get_replies(parent)
|
||||
self.assertEqual(len(replies), 2)
|
||||
self.assertTrue(first_child in replies)
|
||||
self.assertTrue(second_child in replies)
|
||||
|
@ -111,7 +97,7 @@ class TemplateTags(TestCase):
|
|||
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.assertIsInstance(result, models.Review)
|
||||
|
||||
|
@ -119,44 +105,26 @@ class TemplateTags(TestCase):
|
|||
"""did a user like a status"""
|
||||
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"):
|
||||
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, _):
|
||||
"""did a user boost a status"""
|
||||
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"):
|
||||
models.Boost.objects.create(user=self.user, boosted_status=status)
|
||||
self.assertTrue(bookwyrm_tags.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)
|
||||
)
|
||||
self.assertTrue(interaction.get_user_boosted(self.user, status))
|
||||
|
||||
def test_get_boosted(self, _):
|
||||
"""load a boosted status"""
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
status = models.Review.objects.create(user=self.remote_user, book=self.book)
|
||||
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.assertEqual(boosted, status)
|
||||
|
||||
|
@ -178,48 +146,23 @@ class TemplateTags(TestCase):
|
|||
|
||||
def test_get_uuid(self, _):
|
||||
"""uuid functionality"""
|
||||
uuid = bookwyrm_tags.get_uuid("hi")
|
||||
uuid = utilities.get_uuid("hi")
|
||||
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
|
||||
|
||||
def test_get_markdown(self, _):
|
||||
"""mardown format data"""
|
||||
result = bookwyrm_tags.get_markdown("_hi_")
|
||||
result = markdown.get_markdown("_hi_")
|
||||
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>")
|
||||
|
||||
def test_get_mentions(self, _):
|
||||
"""list of people mentioned"""
|
||||
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 ")
|
||||
|
||||
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, _):
|
||||
"""gets the subclass model for a notification status"""
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||
|
|
|
@ -59,7 +59,6 @@ class BookViews(TestCase):
|
|||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
request = self.factory.get("")
|
||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||
is_api.return_value = True
|
||||
result = view(request, self.book.id)
|
||||
|
|
|
@ -60,7 +60,12 @@ class FederationViews(TestCase):
|
|||
|
||||
def test_server_page_block(self):
|
||||
"""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.save()
|
||||
|
||||
|
@ -72,17 +77,32 @@ class FederationViews(TestCase):
|
|||
request.user.is_superuser = True
|
||||
|
||||
view(request, server.id)
|
||||
|
||||
server.refresh_from_db()
|
||||
self.remote_user.refresh_from_db()
|
||||
self.assertEqual(server.status, "blocked")
|
||||
|
||||
# and the user was deactivated
|
||||
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):
|
||||
"""unblock a server"""
|
||||
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.is_active = False
|
||||
self.remote_user.deactivation_reason = "domain_block"
|
||||
|
@ -96,8 +116,15 @@ class FederationViews(TestCase):
|
|||
server.refresh_from_db()
|
||||
self.remote_user.refresh_from_db()
|
||||
self.assertEqual(server.status, "federated")
|
||||
|
||||
# and the user was re-activated
|
||||
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):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
|
|
|
@ -122,6 +122,14 @@ class ListViews(TestCase):
|
|||
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:
|
||||
is_api.return_value = False
|
||||
|
@ -130,6 +138,81 @@ class ListViews(TestCase):
|
|||
result.render()
|
||||
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
|
||||
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||
is_api.return_value = False
|
||||
|
@ -138,12 +221,32 @@ class ListViews(TestCase):
|
|||
result.render()
|
||||
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:
|
||||
is_api.return_value = True
|
||||
result = view(request, self.list.id)
|
||||
self.assertIsInstance(result, ActivitypubResponse)
|
||||
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.user = self.local_user
|
||||
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||
|
@ -204,466 +307,34 @@ class ListViews(TestCase):
|
|||
result = view(request, self.list.id)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
def test_curate_approve(self):
|
||||
"""approve a pending item"""
|
||||
view = views.Curate.as_view()
|
||||
def test_user_lists_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.UserLists.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,
|
||||
models.List.objects.create(name="Public list", user=self.local_user)
|
||||
models.List.objects.create(
|
||||
name="Private list", privacy="direct", user=self.local_user
|
||||
)
|
||||
|
||||
request = self.factory.post(
|
||||
"",
|
||||
{
|
||||
"item": pending.id,
|
||||
"approved": "true",
|
||||
},
|
||||
)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
|
||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
||||
view(request, self.list.id)
|
||||
result = view(request, self.local_user.localname)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
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()
|
||||
def test_user_lists_page_logged_out(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.UserLists.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,
|
||||
models.List.objects.create(name="Public list", user=self.local_user)
|
||||
models.List.objects.create(
|
||||
name="Private list", privacy="direct", user=self.local_user
|
||||
)
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
|
||||
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())
|
||||
result = view(request, self.local_user.username)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
|
529
bookwyrm/tests/views/test_list_actions.py
Normal file
529
bookwyrm/tests/views/test_list_actions.py
Normal 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())
|
|
@ -29,7 +29,7 @@ class Author(View):
|
|||
"author": author,
|
||||
"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")
|
||||
|
@ -43,7 +43,7 @@ class EditAuthor(View):
|
|||
"""info about a book"""
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
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):
|
||||
"""edit a author cool"""
|
||||
|
@ -52,7 +52,7 @@ class EditAuthor(View):
|
|||
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
|
||||
if not form.is_valid():
|
||||
data = {"author": author, "form": form}
|
||||
return TemplateResponse(request, "edit_author.html", data)
|
||||
return TemplateResponse(request, "author/edit_author.html", data)
|
||||
author = form.save()
|
||||
|
||||
return redirect("/author/%s" % author.id)
|
||||
|
|
|
@ -30,6 +30,7 @@ class Book(View):
|
|||
|
||||
def get(self, request, book_id, user_statuses=False):
|
||||
"""info about a book"""
|
||||
user_statuses = user_statuses if request.user.is_authenticated else False
|
||||
try:
|
||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||
except models.Book.DoesNotExist:
|
||||
|
@ -51,9 +52,9 @@ class Book(View):
|
|||
)
|
||||
|
||||
# the reviews to show
|
||||
if user_statuses and request.user.is_authenticated:
|
||||
if user_statuses:
|
||||
if user_statuses == "review":
|
||||
queryset = book.review_set
|
||||
queryset = book.review_set.select_subclasses()
|
||||
elif user_statuses == "comment":
|
||||
queryset = book.comment_set
|
||||
else:
|
||||
|
@ -67,7 +68,9 @@ class Book(View):
|
|||
"book": book,
|
||||
"statuses": paginated.get_page(request.GET.get("page")),
|
||||
"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"],
|
||||
"lists": privacy_filter(
|
||||
request.user, book.list_set.filter(listitem__approved=True)
|
||||
|
|
|
@ -5,7 +5,7 @@ from urllib.parse import urlencode
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
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.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
@ -108,31 +108,23 @@ class List(View):
|
|||
if direction not in ("ascending", "descending"):
|
||||
direction = "ascending"
|
||||
|
||||
internal_sort_by = {
|
||||
directional_sort_by = {
|
||||
"order": "order",
|
||||
"title": "book__title",
|
||||
"rating": "average_rating",
|
||||
}
|
||||
directional_sort_by = internal_sort_by[sort_by]
|
||||
}[sort_by]
|
||||
if direction == "descending":
|
||||
directional_sort_by = "-" + directional_sort_by
|
||||
|
||||
if sort_by == "order":
|
||||
items = book_list.listitem_set.filter(approved=True).order_by(
|
||||
directional_sort_by
|
||||
)
|
||||
elif sort_by == "title":
|
||||
items = book_list.listitem_set.filter(approved=True).order_by(
|
||||
directional_sort_by
|
||||
)
|
||||
elif sort_by == "rating":
|
||||
items = (
|
||||
book_list.listitem_set.annotate(
|
||||
average_rating=Avg(Coalesce("book__review__rating", 0))
|
||||
items = book_list.listitem_set
|
||||
if sort_by == "rating":
|
||||
items = items.annotate(
|
||||
average_rating=Avg(
|
||||
Coalesce("book__review__rating", 0.0),
|
||||
output_field=DecimalField(),
|
||||
)
|
||||
.filter(approved=True)
|
||||
.order_by(directional_sort_by)
|
||||
)
|
||||
items = items.filter(approved=True).order_by(directional_sort_by)
|
||||
|
||||
paginated = Paginator(items, PAGE_LENGTH)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from collections import namedtuple
|
||||
|
||||
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.core.paginator import Paginator
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||
|
@ -37,30 +38,41 @@ class Shelf(View):
|
|||
return HttpResponseNotFound()
|
||||
if not shelf.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
books = shelf.books
|
||||
# this is a constructed "all books" view, with a fake "shelf" obj
|
||||
else:
|
||||
FakeShelf = namedtuple(
|
||||
"Shelf", ("identifier", "name", "user", "books", "privacy")
|
||||
)
|
||||
books = models.Edition.objects.filter(
|
||||
# privacy is ensured because the shelves are already filtered above
|
||||
shelfbook__shelf__in=shelves.all()
|
||||
).distinct()
|
||||
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
||||
|
||||
is_self = request.user == user
|
||||
|
||||
if is_api_request(request):
|
||||
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(
|
||||
shelf.books.order_by("-updated_date"),
|
||||
books.order_by("-updated_date"),
|
||||
PAGE_LENGTH,
|
||||
)
|
||||
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"user": user,
|
||||
"is_self": is_self,
|
||||
"is_self": request.user == user,
|
||||
"shelves": shelves.all(),
|
||||
"shelf": shelf,
|
||||
"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
Loading…
Reference in a new issue