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",
|
"covers_url",
|
||||||
"search_url",
|
"search_url",
|
||||||
"isbn_search_url",
|
"isbn_search_url",
|
||||||
"max_query_count",
|
|
||||||
"name",
|
"name",
|
||||||
"identifier",
|
"identifier",
|
||||||
"local",
|
"local",
|
||||||
|
@ -102,13 +101,6 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
# title we handle separately.
|
# title we handle separately.
|
||||||
self.book_mappings = []
|
self.book_mappings = []
|
||||||
|
|
||||||
def is_available(self):
|
|
||||||
"""check if you're allowed to use this connector"""
|
|
||||||
if self.max_query_count is not None:
|
|
||||||
if self.connector.query_count >= self.max_query_count:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
"""translate arbitrary json into an Activitypub dataclass"""
|
"""translate arbitrary json into an Activitypub dataclass"""
|
||||||
# first, check if we have the origin_id saved
|
# first, check if we have the origin_id saved
|
||||||
|
|
|
@ -87,7 +87,7 @@ def first_search_result(query, min_confidence=0.1):
|
||||||
|
|
||||||
def get_connectors():
|
def get_connectors():
|
||||||
"""load all connectors"""
|
"""load all connectors"""
|
||||||
for info in models.Connector.objects.order_by("priority").all():
|
for info in models.Connector.objects.filter(active=True).order_by("priority").all():
|
||||||
yield load_connector(info)
|
yield load_connector(info)
|
||||||
|
|
||||||
|
|
||||||
|
|
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
|
from .fields import RemoteIdField
|
||||||
|
|
||||||
|
|
||||||
|
DeactivationReason = models.TextChoices(
|
||||||
|
"DeactivationReason",
|
||||||
|
[
|
||||||
|
"self_deletion",
|
||||||
|
"moderator_deletion",
|
||||||
|
"domain_block",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BookWyrmModel(models.Model):
|
class BookWyrmModel(models.Model):
|
||||||
"""shared fields"""
|
"""shared fields"""
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from bookwyrm.connectors.settings import CONNECTORS
|
from bookwyrm.connectors.settings import CONNECTORS
|
||||||
|
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel, DeactivationReason
|
||||||
|
|
||||||
|
|
||||||
ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
|
ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
|
||||||
|
@ -17,6 +17,10 @@ class Connector(BookWyrmModel):
|
||||||
local = models.BooleanField(default=False)
|
local = models.BooleanField(default=False)
|
||||||
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
||||||
api_key = models.CharField(max_length=255, null=True, blank=True)
|
api_key = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
active = models.BooleanField(default=True)
|
||||||
|
deactivation_reason = models.CharField(
|
||||||
|
max_length=255, choices=DeactivationReason.choices, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
base_url = models.CharField(max_length=255)
|
base_url = models.CharField(max_length=255)
|
||||||
books_url = models.CharField(max_length=255)
|
books_url = models.CharField(max_length=255)
|
||||||
|
@ -24,13 +28,6 @@ class Connector(BookWyrmModel):
|
||||||
search_url = models.CharField(max_length=255, null=True, blank=True)
|
search_url = models.CharField(max_length=255, null=True, blank=True)
|
||||||
isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
|
isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
politeness_delay = models.IntegerField(null=True, blank=True) # seconds
|
|
||||||
max_query_count = models.IntegerField(null=True, blank=True)
|
|
||||||
# how many queries executed in a unit of time, like a day
|
|
||||||
query_count = models.IntegerField(default=0)
|
|
||||||
# when to reset the query count back to 0 (ie, after 1 day)
|
|
||||||
query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} ({})".format(
|
return "{} ({})".format(
|
||||||
self.identifier,
|
self.identifier,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" connections to external ActivityPub servers """
|
""" connections to external ActivityPub servers """
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
@ -34,6 +35,13 @@ class FederatedServer(BookWyrmModel):
|
||||||
is_active=False, deactivation_reason="domain_block"
|
is_active=False, deactivation_reason="domain_block"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# check for related connectors
|
||||||
|
if self.application_type == "bookwyrm":
|
||||||
|
connector_model = apps.get_model("bookwyrm.Connector", require_ready=True)
|
||||||
|
connector_model.objects.filter(
|
||||||
|
identifier=self.server_name, active=True
|
||||||
|
).update(active=False, deactivation_reason="domain_block")
|
||||||
|
|
||||||
def unblock(self):
|
def unblock(self):
|
||||||
"""unblock a server"""
|
"""unblock a server"""
|
||||||
self.status = "federated"
|
self.status = "federated"
|
||||||
|
@ -43,6 +51,15 @@ class FederatedServer(BookWyrmModel):
|
||||||
is_active=True, deactivation_reason=None
|
is_active=True, deactivation_reason=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# check for related connectors
|
||||||
|
if self.application_type == "bookwyrm":
|
||||||
|
connector_model = apps.get_model("bookwyrm.Connector", require_ready=True)
|
||||||
|
connector_model.objects.filter(
|
||||||
|
identifier=self.server_name,
|
||||||
|
active=False,
|
||||||
|
deactivation_reason="domain_block",
|
||||||
|
).update(active=True, deactivation_reason=None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_blocked(cls, url):
|
def is_blocked(cls, url):
|
||||||
"""look up if a domain is blocked"""
|
"""look up if a domain is blocked"""
|
||||||
|
|
|
@ -19,21 +19,11 @@ from bookwyrm.signatures import create_key_pair
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
|
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel, DeactivationReason
|
||||||
from .federated_server import FederatedServer
|
from .federated_server import FederatedServer
|
||||||
from . import fields, Review
|
from . import fields, Review
|
||||||
|
|
||||||
|
|
||||||
DeactivationReason = models.TextChoices(
|
|
||||||
"DeactivationReason",
|
|
||||||
[
|
|
||||||
"self_deletion",
|
|
||||||
"moderator_deletion",
|
|
||||||
"domain_block",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class User(OrderedCollectionPageMixin, AbstractUser):
|
class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
"""a user who wants to read books"""
|
"""a user who wants to read books"""
|
||||||
|
|
||||||
|
|
|
@ -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="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
||||||
<p><label class="label" for="id_name">{% trans "Name:" %}</label> {{ form.name }}</p>
|
<p class="mb-2"><label class="label" for="id_name">{% trans "Name:" %}</label> {{ form.name }}</p>
|
||||||
{% for error in form.name.errors %}
|
{% for error in form.name.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p><label class="label" for="id_bio">{% trans "Bio:" %}</label> {{ form.bio }}</p>
|
<p class="mb-2">
|
||||||
|
<label class="label" for="id_aliases">{% trans "Aliases:" %}</label>
|
||||||
|
{{ form.aliases }}
|
||||||
|
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||||
|
</p>
|
||||||
|
{% for error in form.aliases.errors %}
|
||||||
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<p class="mb-2"><label class="label" for="id_bio">{% trans "Bio:" %}</label> {{ form.bio }}</p>
|
||||||
{% for error in form.bio.errors %}
|
{% for error in form.bio.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
|
<p class="mb-2"><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
|
||||||
{% for error in form.wikipedia_link.errors %}
|
{% for error in form.wikipedia_link.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p><label class="label" for="id_born">{% trans "Birth date:" %}</label> {{ form.born }}</p>
|
<p class="mb-2">
|
||||||
|
<label class="label" for="id_born">{% trans "Birth date:" %}</label>
|
||||||
|
<input type="date" name="born" value="{{ form.born.value|date:'Y-m-d' }}" class="input" id="id_born">
|
||||||
|
</p>
|
||||||
{% for error in form.born.errors %}
|
{% for error in form.born.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p><label class="label" for="id_died">{% trans "Death date:" %}</label> {{ form.died }}</p>
|
<p class="mb-2">
|
||||||
|
<label class="label" for="id_died">{% trans "Death date:" %}</label>
|
||||||
|
<input type="date" name="died" value="{{ form.died.value|date:'Y-m-d' }}" class="input" id="id_died">
|
||||||
|
</p>
|
||||||
{% for error in form.died.errors %}
|
{% for error in form.died.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h2 class="title is-4">{% trans "Author Identifiers" %}</h2>
|
<h2 class="title is-4">{% trans "Author Identifiers" %}</h2>
|
||||||
<p><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }}</p>
|
<p class="mb-2"><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }}</p>
|
||||||
{% for error in form.openlibrary_key.errors %}
|
{% for error in form.openlibrary_key.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p><label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label> {{ form.librarything_key }}</p>
|
<p class="mb-2"><label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label> {{ form.inventaire_id }}</p>
|
||||||
|
{% for error in form.inventaire_id.errors %}
|
||||||
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<p class="mb-2"><label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label> {{ form.librarything_key }}</p>
|
||||||
{% for error in form.librarything_key.errors %}
|
{% for error in form.librarything_key.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p><label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label> {{ form.goodreads_key }}</p>
|
<p class="mb-2"><label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label> {{ form.goodreads_key }}</p>
|
||||||
{% for error in form.goodreads_key.errors %}
|
{% for error in form.goodreads_key.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
|
@ -38,9 +38,8 @@
|
||||||
{% if user_authenticated and can_edit_book %}
|
{% if user_authenticated and can_edit_book %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a href="{{ book.id }}/edit">
|
<a href="{{ book.id }}/edit">
|
||||||
<span class="icon icon-pencil" title="{% trans "Edit Book" %}">
|
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
|
||||||
<span class="is-sr-only">{% trans "Edit Book" %}</span>
|
<span>{% trans "Edit Book" %}</span>
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -163,12 +162,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Add read dates" as button_text %}
|
{% trans "Add read dates" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon="plus" class="is-small" controls_text="add-readthrough" %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add-readthrough" focus="add-readthrough-focus" %}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% if not readthroughs.exists %}
|
|
||||||
<p>{% trans "You don't have any reading activity for this book." %}</p>
|
|
||||||
{% endif %}
|
|
||||||
<section class="is-hidden box" id="add-readthrough">
|
<section class="is-hidden box" id="add-readthrough">
|
||||||
<form name="add-readthrough" action="/create-readthrough" method="post">
|
<form name="add-readthrough" action="/create-readthrough" method="post">
|
||||||
{% include 'snippets/readthrough_form.html' with readthrough=None %}
|
{% include 'snippets/readthrough_form.html' with readthrough=None %}
|
||||||
|
@ -183,6 +179,9 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
{% if not readthroughs.exists %}
|
||||||
|
<p>{% trans "You don't have any reading activity for this book." %}</p>
|
||||||
|
{% endif %}
|
||||||
{% for readthrough in readthroughs %}
|
{% for readthrough in readthroughs %}
|
||||||
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
|
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -195,7 +194,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="block" id="reviews">
|
<div class="block" id="reviews">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
{% if user_statuses.review_count or user_statuses.comment_count or user_stuatses.quotation_count %}
|
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
{% url 'book' book.id as tab_url %}
|
{% url 'book' book.id as tab_url %}
|
||||||
|
@ -225,46 +224,26 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for review in statuses %}
|
{% for status in statuses %}
|
||||||
<div
|
<div
|
||||||
class="block"
|
class="block"
|
||||||
|
{% if status.status_type == 'Review' or status.status_type == 'Rating' %}
|
||||||
itemprop="review"
|
itemprop="review"
|
||||||
itemscope
|
itemscope
|
||||||
itemtype="https://schema.org/Review"
|
itemtype="https://schema.org/Review"
|
||||||
|
{% endif %}
|
||||||
>
|
>
|
||||||
{% with status=review hide_book=True depth=1 %}
|
{% include 'snippets/status/status.html' with status=status hide_book=True depth=1 %}
|
||||||
{% include 'snippets/status/status.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if ratings %}
|
||||||
<div class="block is-flex is-flex-wrap-wrap">
|
<div class="block is-flex is-flex-wrap-wrap">
|
||||||
{% for rating in ratings %}
|
{% for rating in ratings %}
|
||||||
{% with user=rating.user %}
|
{% include 'book/rating.html' with user=rating.user rating=rating %}
|
||||||
<div class="block mr-5">
|
|
||||||
<div class="media">
|
|
||||||
<div class="media-left">
|
|
||||||
{% include 'snippets/avatar.html' %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="media-content">
|
|
||||||
<div>
|
|
||||||
<a href="{{ user.local_path }}">{{ user.display_name }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
|
||||||
<p class="mr-1">{% trans "rated it" %}</p>
|
|
||||||
|
|
||||||
{% include 'snippets/stars.html' with rating=rating.rating %}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
|
{% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -125,7 +125,7 @@
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
||||||
{{ form.publishers }}
|
{{ form.publishers }}
|
||||||
<span class="help">{% trans "Separate multiple publishers with commas." %}</span>
|
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||||
</p>
|
</p>
|
||||||
{% for error in form.publishers.errors %}
|
{% for error in form.publishers.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
@ -162,7 +162,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<label class="label" for="id_add_author">{% trans "Add Authors:" %}</label>
|
<label class="label" for="id_add_author">{% trans "Add Authors:" %}</label>
|
||||||
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
|
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
|
||||||
<p class="help">Separate multiple author names with commas.</p>
|
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
|
|
||||||
{% block title %}{% blocktrans with book_title=work.title %}Editions of {{ book_title }}{% endblocktrans %}{% endblock %}
|
{% block title %}{% blocktrans with book_title=work.title %}Editions of {{ book_title }}{% endblocktrans %}{% endblock %}
|
||||||
|
|
||||||
|
|
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 %}
|
{% spaceless %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% with 0|uuid as uuid %}
|
{% with 0|uuid as uuid %}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% block title %}{% trans "Compose status" %}{% endblock %}
|
{% block title %}{% trans "Compose status" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
{% load markdown %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
<div class="card is-stretchable">
|
<div class="card is-stretchable">
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if user.summary %}
|
{% if user.summary %}
|
||||||
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
|
{{ user.summary|to_markdown|safe|truncatechars_html:40 }}
|
||||||
{% else %} {% endif %}
|
{% else %} {% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load markdown %}
|
||||||
|
|
||||||
{% block title %}{% trans "Welcome" %}{% endblock %}
|
{% block title %}{% trans "Welcome" %}{% endblock %}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<h2 class="title">{% trans "This instance is closed" %}</h2>
|
<h2 class="title">{% trans "This instance is closed" %}</h2>
|
||||||
<p>{{ site.registration_closed_text | safe}}</p>
|
<p>{{ site.registration_closed_text|safe}}</p>
|
||||||
|
|
||||||
{% if site.allow_invite_requests %}
|
{% if site.allow_invite_requests %}
|
||||||
{% if request_received %}
|
{% if request_received %}
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
<label for="id_request_email" class="label">{% trans "Email address:" %}</label>
|
<label for="id_request_email" class="label">{% trans "Email address:" %}</label>
|
||||||
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
|
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
|
||||||
{% for error in request_form.email.errors %}
|
{% for error in request_form.email.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error|escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
{% include 'user/user_preview.html' with user=request.user %}
|
{% include 'user/user_preview.html' with user=request.user %}
|
||||||
{% if request.user.summary %}
|
{% if request.user.summary %}
|
||||||
<div class="box content">
|
<div class="box content">
|
||||||
{{ request.user.summary | to_markdown | safe }}
|
{{ request.user.summary|to_markdown|safe }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load markdown %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if book %}
|
{% if book %}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{% extends 'feed/feed_layout.html' %}
|
{% extends 'feed/feed_layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load humanize %}
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Updates" %}{% endblock %}
|
{% block title %}{% trans "Updates" %}{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
<div class="columns is-mobile scroll-x mb-0">
|
<div class="columns is-mobile scroll-x mb-0">
|
||||||
{% for user in suggested_users %}
|
{% for user in suggested_users %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load status_display %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
|
||||||
{% with depth=depth|add:1 %}
|
{% with depth=depth|add:1 %}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{% if is_self and goal %}
|
{% if is_self and goal %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Edit Goal" as button_text %}
|
{% trans "Edit Goal" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="show-edit-goal" focus="edit-form-header" %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="show-edit-goal" focus="edit-form-header" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block title %}{% trans "Import Status" %}{% endblock %}
|
{% block title %}{% trans "Import Status" %}{% endblock %}
|
||||||
|
@ -54,8 +53,8 @@
|
||||||
<input class="checkbox" type="checkbox" name="import_item" value="{{ item.id }}" id="import-item-{{ item.id }}">
|
<input class="checkbox" type="checkbox" name="import_item" value="{{ item.id }}" id="import-item-{{ item.id }}">
|
||||||
<label for="import-item-{{ item.id }}">
|
<label for="import-item-{{ item.id }}">
|
||||||
Line {{ item.index }}:
|
Line {{ item.index }}:
|
||||||
<strong>{{ item.data|dict_key:'Title' }}</strong> by
|
<strong>{{ item.data.Title }}</strong> by
|
||||||
{{ item.data|dict_key:'Author' }}
|
{{ item.data.Author }}
|
||||||
</label>
|
</label>
|
||||||
<p>
|
<p>
|
||||||
{{ item.fail_reason }}.
|
{{ item.fail_reason }}.
|
||||||
|
@ -90,8 +89,8 @@
|
||||||
<li class="pb-1">
|
<li class="pb-1">
|
||||||
<p>
|
<p>
|
||||||
Line {{ item.index }}:
|
Line {{ item.index }}:
|
||||||
<strong>{{ item.data|dict_key:'Title' }}</strong> by
|
<strong>{{ item.data.Title }}</strong> by
|
||||||
{{ item.data|dict_key:'Author' }}
|
{{ item.data.Author }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ item.fail_reason }}.
|
{{ item.fail_reason }}.
|
||||||
|
@ -130,10 +129,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ item.data|dict_key:'Title' }}
|
{{ item.data.Title }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ item.data|dict_key:'Author' }}
|
{{ item.data.Author }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.book %}
|
{% if item.book %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load layout %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{% get_lang %}">
|
<html lang="{% get_lang %}">
|
||||||
|
@ -214,7 +214,7 @@
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
{% trans 'BookWyrm is open source software. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.' %}
|
{% blocktrans %}BookWyrm's source code is freely available. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% if site.footer_item %}
|
{% if site.footer_item %}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
{% extends 'lists/list_layout.html' %}
|
{% extends 'lists/list_layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load markdown %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
{% if request.user == list.user and pending_count %}
|
{% if request.user == list.user and pending_count %}
|
||||||
<div class="block content">
|
<div class="block content">
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count | pluralize }} awaiting your approval</a>
|
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count|pluralize }} awaiting your approval</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -50,9 +51,9 @@
|
||||||
<p>
|
<p>
|
||||||
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
|
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div>
|
||||||
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
||||||
</p>
|
</div>
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' %}
|
{% include 'snippets/shelve_button/shelve_button.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load markdown %}
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
{% for list in lists %}
|
{% for list in lists %}
|
||||||
<div class="column is-one-quarter">
|
<div class="column is-one-quarter">
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<div class="card-content is-flex-grow-0">
|
<div class="card-content is-flex-grow-0">
|
||||||
<div {% if list.description %}title="{{ list.description }}"{% endif %}>
|
<div class="is-clipped" {% if list.description %}title="{{ list.description }}"{% endif %}>
|
||||||
{% if list.description %}
|
{% if list.description %}
|
||||||
{{ list.description|to_markdown|safe|truncatechars_html:30 }}
|
{{ list.description|to_markdown|safe|truncatechars_html:30 }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
|
|
||||||
{% block title %}{{ list.name }}{% endblock %}
|
{% block title %}{{ list.name }}{% endblock %}
|
||||||
|
|
||||||
|
@ -16,7 +15,7 @@
|
||||||
{% if request.user == list.user %}
|
{% if request.user == list.user %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Edit List" as button_text %}
|
{% trans "Edit List" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-list" focus="edit-list-header" %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit-list" focus="edit-list-header" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Lists" %}{% endblock %}
|
{% block title %}{% trans "Lists" %}{% endblock %}
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Create List" as button_text %}
|
{% trans "Create List" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon="plus" text=button_text focus="create-list-header" %}
|
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon_with_text="plus" text=button_text focus="create-list-header" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -38,6 +38,9 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
<div class="box has-background-primary-light">
|
<div class="box has-background-primary-light">
|
||||||
{% if site.allow_registration %}
|
{% if site.allow_registration %}
|
||||||
<h2 class="title">{% trans "Create an Account" %}</h2>
|
<h2 class="title">{% trans "Create an Account" %}</h2>
|
||||||
|
@ -50,15 +53,15 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
<div class="block">
|
||||||
<div class="block">
|
<div class="box">
|
||||||
{% include 'snippets/about.html' %}
|
{% include 'snippets/about.html' %}
|
||||||
|
|
||||||
<p class="block">
|
<p class="block">
|
||||||
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
|
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/admin_layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
|
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
|
||||||
|
@ -29,7 +28,7 @@
|
||||||
<a href="{{ comment.user.local_path }}">{{ comment.user.display_name }}</a>
|
<a href="{{ comment.user.local_path }}">{{ comment.user.display_name }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
{{ comment.created_date | naturaltime }}
|
{{ comment.created_date|naturaltime }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
{% block title %}{% trans "Notifications" %}{% endblock %}
|
{% block title %}{% trans "Notifications" %}{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
{% with results|first as local_results %}
|
{% with results|first as local_results %}
|
||||||
<ul class="block">
|
<ul class="block">
|
||||||
{% for result in local_results.results %}
|
{% for result in local_results.results %}
|
||||||
<li class="pd-4">
|
<li class="pd-4 mb-5">
|
||||||
{% include 'snippets/search_result_text.html' with result=result %}
|
{% include 'snippets/search_result_text.html' with result=result %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{% extends 'search/layout.html' %}
|
{% extends 'search/layout.html' %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/admin_layout.html' %}
|
||||||
{% block title %}{{ server.server_name }}{% endblock %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load markdown %}
|
||||||
|
|
||||||
|
{% block title %}{{ server.server_name }}{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{{ server.server_name }}
|
{{ server.server_name }}
|
||||||
|
@ -14,60 +15,64 @@
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<section class="column is-half content">
|
<section class="column is-half is-flex is-flex-direction-column">
|
||||||
<h2 class="title is-4">{% trans "Details" %}</h2>
|
<h2 class="title is-4">{% trans "Details" %}</h2>
|
||||||
<dl>
|
<div class="box is-flex-grow-1 content">
|
||||||
<div class="is-flex">
|
<dl>
|
||||||
<dt>{% trans "Software:" %}</dt>
|
<div class="is-flex">
|
||||||
<dd>{{ server.application_type }}</dd>
|
<dt>{% trans "Software:" %}</dt>
|
||||||
</div>
|
<dd>{{ server.application_type }}</dd>
|
||||||
<div class="is-flex">
|
</div>
|
||||||
<dt>{% trans "Version:" %}</dt>
|
<div class="is-flex">
|
||||||
<dd>{{ server.application_version }}</dd>
|
<dt>{% trans "Version:" %}</dt>
|
||||||
</div>
|
<dd>{{ server.application_version }}</dd>
|
||||||
<div class="is-flex">
|
</div>
|
||||||
<dt>{% trans "Status:" %}</dt>
|
<div class="is-flex">
|
||||||
<dd>{{ server.status }}</dd>
|
<dt>{% trans "Status:" %}</dt>
|
||||||
</div>
|
<dd>{{ server.status }}</dd>
|
||||||
</dl>
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="column is-half content">
|
<section class="column is-half is-flex is-flex-direction-column">
|
||||||
<h2 class="title is-4">{% trans "Activity" %}</h2>
|
<h2 class="title is-4">{% trans "Activity" %}</h2>
|
||||||
<dl>
|
<div class="box is-flex-grow-1 content">
|
||||||
<div class="is-flex">
|
<dl>
|
||||||
<dt>{% trans "Users:" %}</dt>
|
<div class="is-flex">
|
||||||
<dd>
|
<dt>{% trans "Users:" %}</dt>
|
||||||
{{ users.count }}
|
<dd>
|
||||||
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
{{ users.count }}
|
||||||
</dd>
|
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||||
</div>
|
</dd>
|
||||||
<div class="is-flex">
|
</div>
|
||||||
<dt>{% trans "Reports:" %}</dt>
|
<div class="is-flex">
|
||||||
<dd>
|
<dt>{% trans "Reports:" %}</dt>
|
||||||
{{ reports.count }}
|
<dd>
|
||||||
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
{{ reports.count }}
|
||||||
</dd>
|
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||||
</div>
|
</dd>
|
||||||
<div class="is-flex">
|
</div>
|
||||||
<dt>{% trans "Followed by us:" %}</dt>
|
<div class="is-flex">
|
||||||
<dd>
|
<dt>{% trans "Followed by us:" %}</dt>
|
||||||
{{ followed_by_us.count }}
|
<dd>
|
||||||
</dd>
|
{{ followed_by_us.count }}
|
||||||
</div>
|
</dd>
|
||||||
<div class="is-flex">
|
</div>
|
||||||
<dt>{% trans "Followed by them:" %}</dt>
|
<div class="is-flex">
|
||||||
<dd>
|
<dt>{% trans "Followed by them:" %}</dt>
|
||||||
{{ followed_by_them.count }}
|
<dd>
|
||||||
</dd>
|
{{ followed_by_them.count }}
|
||||||
</div>
|
</dd>
|
||||||
<div class="is-flex">
|
</div>
|
||||||
<dt>{% trans "Blocked by us:" %}</dt>
|
<div class="is-flex">
|
||||||
<dd>
|
<dt>{% trans "Blocked by us:" %}</dt>
|
||||||
{{ blocked_by_us.count }}
|
<dd>
|
||||||
</dd>
|
{{ blocked_by_us.count }}
|
||||||
</div>
|
</dd>
|
||||||
</dl>
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -78,11 +83,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Edit" as button_text %}
|
{% trans "Edit" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-notes" %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit-notes" %}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% if server.notes %}
|
{% if server.notes %}
|
||||||
<p id="hide-edit-notes">{{ server.notes|to_markdown|safe }}</p>
|
<div class="box" id="hide-edit-notes">{{ server.notes|to_markdown|safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes">
|
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -6,9 +6,8 @@
|
||||||
|
|
||||||
{% block edit-button %}
|
{% block edit-button %}
|
||||||
<a href="{% url 'settings-import-blocklist' %}">
|
<a href="{% url 'settings-import-blocklist' %}">
|
||||||
<span class="icon icon-plus" title="{% trans 'Add server' %}">
|
<span class="icon icon-plus" title="{% trans 'Add server' %}" aria-hidden="True"></span>
|
||||||
<span class="is-sr-only">{% trans "Add server" %}</span>
|
<span>{% trans "Add server" %}</span>
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">
|
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<figure
|
<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 i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
{% if book.authors %}
|
{% if book.authors %}
|
||||||
{% blocktrans with path=book.local_path title=book|title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
|
{% blocktrans with path=book.local_path title=book|title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load interaction %}
|
||||||
|
{% load utilities %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% with status.id|uuid as uuid %}
|
{% with status.id|uuid as uuid %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% with status_type=request.GET.status_type %}
|
{% with status_type=request.GET.status_type %}
|
||||||
<div class="tab-group">
|
<div class="tab-group">
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load utilities %}
|
||||||
|
{% load status_display %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{% if type == 'direct' %}status{% else %}{{ type }}{% endif %}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}">
|
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{% if type == 'direct' %}status{% else %}{{ type }}{% endif %}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -100,7 +103,7 @@
|
||||||
|
|
||||||
{# bottom bar #}
|
{# bottom bar #}
|
||||||
<input type="checkbox" class="is-hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true">
|
<input type="checkbox" class="is-hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true">
|
||||||
|
|
||||||
<div class="columns mt-1">
|
<div class="columns mt-1">
|
||||||
<div class="field has-addons column">
|
<div class="field has-addons column">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load interaction %}
|
||||||
|
{% load utilities %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% with status.id|uuid as uuid %}
|
{% with status.id|uuid as uuid %}
|
||||||
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
|
|
||||||
<span class="column is-narrow pb-0">
|
<span class="column is-narrow pb-0">
|
||||||
{% trans "Show filters" as text %}
|
{% trans "Show filters" as text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=text controls_text="filters" icon="arrow-down" class="is-small" focus="filters" %}
|
{% include 'snippets/toggle/open_button.html' with text=text controls_text="filters" icon_with_text="arrow-down" class="is-small" focus="filters" %}
|
||||||
{% trans "Hide filters" as text %}
|
{% trans "Hide filters" as text %}
|
||||||
{% include 'snippets/toggle/close_button.html' with text=text controls_text="filters" icon="x" class="is-small" %}
|
{% include 'snippets/toggle/close_button.html' with text=text controls_text="filters" icon_with_text="arrow-up" class="is-small" %}
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% if request.user in user.follow_requests.all %}
|
||||||
{% if request.user|follow_request_exists:user %}
|
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<form action="/accept-follow-request/" method="POST">
|
<form action="/accept-follow-request/" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
|
|
||||||
<div class="
|
<div class="
|
||||||
field is-grouped
|
field is-grouped
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
<div class="select {{ class }}">
|
<div class="select {{ class }}">
|
||||||
{% with 0|uuid as uuid %}
|
{% with 0|uuid as uuid %}
|
||||||
{% if not no_label %}
|
{% if not no_label %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">
|
<label class="label" tabindex="0" id="add-readthrough-focus">
|
||||||
{% trans "Started reading" %}
|
{% trans "Started reading" %}
|
||||||
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
|
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% with 0|uuid as report_uuid %}
|
{% with 0|uuid as report_uuid %}
|
||||||
|
|
||||||
{% trans "Report" as button_text %}
|
{% trans "Report" as button_text %}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
|
|
||||||
{% with book.id|uuid as uuid %}
|
{% with book.id|uuid as uuid %}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load utilities %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% for shelf in shelves %}
|
{% for shelf in shelves %}
|
||||||
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
||||||
{% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %}
|
{% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load markdown %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% with status_type=status.status_type %}
|
{% with status_type=status.status_type %}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load markdown %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if not hide_book %}
|
{% if not hide_book %}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{% extends 'components/card.html' %}
|
{% extends 'components/card.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
{% load humanize %}
|
|
||||||
|
|
||||||
{% block card-header %}
|
{% block card-header %}
|
||||||
<div class="card-header-title has-background-white-ter is-block">
|
<div class="card-header-title has-background-white-ter is-block">
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load status_display %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if not status.deleted %}
|
{% if not status.deleted %}
|
||||||
{% if status.status_type == 'Announce' %}
|
{% if status.status_type == 'Announce' %}
|
||||||
<a href="{{ status.user.local_path }}">
|
<a href="{{ status.user.local_path }}">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
{% load status_display %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{% if status.status_type == 'GeneratedNote' %}
|
{% if status.status_type == 'GeneratedNote' %}
|
||||||
{{ status.content | safe }}
|
{{ status.content|safe }}
|
||||||
{% elif status.status_type == 'Rating' %}
|
{% elif status.status_type == 'Rating' %}
|
||||||
{% trans "rated" %}
|
{% trans "rated" %}
|
||||||
{% elif status.status_type == 'Review' %}
|
{% elif status.status_type == 'Review' %}
|
||||||
|
@ -91,7 +92,7 @@
|
||||||
|
|
||||||
</h3>
|
</h3>
|
||||||
<p class="is-size-7 is-flex is-align-items-center">
|
<p class="is-size-7 is-flex is-align-items-center">
|
||||||
<a href="{{ status.remote_id }}">{{ status.published_date|timesince }}</a>
|
<a href="{{ status.remote_id }}">{{ status.published_date|published_date }}</a>
|
||||||
{% if status.progress %}
|
{% if status.progress %}
|
||||||
<span class="ml-1">
|
<span class="ml-1">
|
||||||
{% if status.progress_mode == 'PG' %}
|
{% if status.progress_mode == 'PG' %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'components/dropdown.html' %}
|
{% extends 'components/dropdown.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% block dropdown-trigger %}
|
{% block dropdown-trigger %}
|
||||||
<span class="icon icon-dots-three m-0-mobile"></span>
|
<span class="icon icon-dots-three m-0-mobile"></span>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load markdown %}
|
||||||
|
{% load utilities %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% with 0|uuid as uuid %}
|
{% with 0|uuid as uuid %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'components/dropdown.html' %}
|
{% extends 'components/dropdown.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% block dropdown-trigger %}
|
{% block dropdown-trigger %}
|
||||||
<span class="icon icon-dots-three">
|
<span class="icon icon-dots-three">
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
{% load markdown %}
|
||||||
|
|
||||||
{% block title %}{{ user.display_name }}{% endblock %}
|
{% block title %}{{ user.display_name }}{% endblock %}
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@
|
||||||
|
|
||||||
{% if user.summary %}
|
{% if user.summary %}
|
||||||
<div class="column box has-background-white-bis content">
|
<div class="column box has-background-white-bis content">
|
||||||
{{ user.summary | to_markdown | safe }}
|
{{ user.summary|to_markdown|safe }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
{% if is_self %}
|
{% if is_self %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Create list" as button_text %}
|
{% trans "Create list" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon="plus" text=button_text %}
|
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon_with_text="plus" text=button_text %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'user/layout.html' %}
|
{% extends 'user/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
{% with user|username as username %}
|
{% with user|username as username %}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends 'user/layout.html' %}
|
{% extends 'user/layout.html' %}
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load utilities %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@
|
||||||
{% if is_self %}
|
{% if is_self %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Create shelf" as button_text %}
|
{% trans "Create shelf" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon="plus" controls_text="create-shelf-form" focus="create-shelf-form-header" %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" controls_text="create-shelf-form" focus="create-shelf-form-header" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,7 +59,7 @@
|
||||||
{% if is_self and shelf.id %}
|
{% if is_self and shelf.id %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Edit shelf" as button_text %}
|
{% trans "Edit shelf" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-shelf-form" focus="edit-shelf-form-header" %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit-shelf-form" focus="edit-shelf-form-header" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,7 +80,9 @@
|
||||||
<th>{% trans "Shelved" %}</th>
|
<th>{% trans "Shelved" %}</th>
|
||||||
<th>{% trans "Started" %}</th>
|
<th>{% trans "Started" %}</th>
|
||||||
<th>{% trans "Finished" %}</th>
|
<th>{% trans "Finished" %}</th>
|
||||||
{% if ratings %}<th>{% trans "Rating" %}</th>{% endif %}
|
{% if request.user.is_authenticated %}
|
||||||
|
<th>{% trans "Rating" %}</th>
|
||||||
|
{% endif %}
|
||||||
{% if shelf.user == request.user %}
|
{% if shelf.user == request.user %}
|
||||||
<th aria-hidden="true"></th>
|
<th aria-hidden="true"></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -99,18 +102,18 @@
|
||||||
{% include 'snippets/authors.html' %}
|
{% include 'snippets/authors.html' %}
|
||||||
</td>
|
</td>
|
||||||
<td data-title="{% trans "Shelved" %}">
|
<td data-title="{% trans "Shelved" %}">
|
||||||
{{ book.created_date | naturalday }}
|
{{ book.created_date|naturalday }}
|
||||||
</td>
|
</td>
|
||||||
{% latest_read_through book user as read_through %}
|
{% latest_read_through book user as read_through %}
|
||||||
<td data-title="{% trans "Started" %}">
|
<td data-title="{% trans "Started" %}">
|
||||||
{{ read_through.start_date | naturalday |default_if_none:""}}
|
{{ read_through.start_date|naturalday|default_if_none:""}}
|
||||||
</td>
|
</td>
|
||||||
<td data-title="{% trans "Finished" %}">
|
<td data-title="{% trans "Finished" %}">
|
||||||
{{ read_through.finish_date | naturalday |default_if_none:""}}
|
{{ read_through.finish_date|naturalday|default_if_none:""}}
|
||||||
</td>
|
</td>
|
||||||
{% if ratings %}
|
{% if request.user.is_authenticated %}
|
||||||
<td data-title="{% trans "Rating" %}">
|
<td data-title="{% trans "Rating" %}">
|
||||||
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
|
{% include 'snippets/stars.html' with rating=book.rating %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if shelf.user == request.user %}
|
{% if shelf.user == request.user %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'user/layout.html' %}
|
{% extends 'user/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% block title %}{{ user.display_name }}{% endblock %}
|
{% block title %}{{ user.display_name }}{% endblock %}
|
||||||
|
|
||||||
|
@ -12,9 +12,8 @@
|
||||||
{% if is_self %}
|
{% if is_self %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a href="{% url 'prefs-profile' %}">
|
<a href="{% url 'prefs-profile' %}">
|
||||||
<span class="icon icon-pencil" title="Edit profile">
|
<span class="icon icon-pencil" title="Edit profile" aria-hidden="true"></span>
|
||||||
<span class="is-sr-only">{% trans "Edit profile" %}</span>
|
<span>{% trans "Edit profile" %}</span>
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -59,8 +58,9 @@
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<h2 class="title column">{% trans "User Activity" %}</h2>
|
<h2 class="title column">{% trans "User Activity" %}</h2>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a class="icon icon-rss" target="_blank" href="{{ user.local_path }}/rss">
|
<a target="_blank" href="{{ user.local_path }}/rss">
|
||||||
<span class="is-sr-only">{% trans "RSS feed" %}</span>
|
<span class="icon icon-rss" aria-hidden="true"></span>
|
||||||
|
<span>{% trans "RSS feed" %}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
{% load utilities %}
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/admin_layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load humanize %}
|
|
||||||
|
|
||||||
{% block title %}{{ user.username }}{% endblock %}
|
{% block title %}{{ user.username }}{% endblock %}
|
||||||
{% block header %}{{ user.username }}{% endblock %}
|
{% block header %}{{ user.username }}{% endblock %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load markdown %}
|
||||||
<div class="block columns">
|
<div class="block columns">
|
||||||
<div class="column is-flex is-flex-direction-column">
|
<div class="column is-flex is-flex-direction-column">
|
||||||
<h4 class="title is-4">{% trans "User details" %}</h4>
|
<h4 class="title is-4">{% trans "User details" %}</h4>
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
{% include 'user/user_preview.html' with user=user %}
|
{% include 'user/user_preview.html' with user=user %}
|
||||||
{% if user.summary %}
|
{% if user.summary %}
|
||||||
<div class="box content has-background-white-ter is-shadowless">
|
<div class="box content has-background-white-ter is-shadowless">
|
||||||
{{ user.summary | to_markdown | safe }}
|
{{ user.summary|to_markdown|safe }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
""" template filters """
|
""" template filters """
|
||||||
from uuid import uuid4
|
from django import template
|
||||||
|
|
||||||
from django import template, utils
|
|
||||||
from django.db.models import Avg
|
from django.db.models import Avg
|
||||||
|
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
from bookwyrm.views.status import to_markdown
|
from bookwyrm.views.status import to_markdown
|
||||||
|
from bookwyrm.templatetags.utilities import get_user_identifier
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="dict_key")
|
|
||||||
def dict_key(d, k):
|
|
||||||
"""Returns the given key from a dictionary."""
|
|
||||||
return d.get(k) or 0
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="rating")
|
@register.filter(name="rating")
|
||||||
def get_rating(book, user):
|
def get_rating(book, user):
|
||||||
"""get the overall rating of a book"""
|
"""get the overall rating of a book"""
|
||||||
|
@ -43,119 +36,12 @@ def get_user_rating(book, user):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="username")
|
|
||||||
def get_user_identifier(user):
|
|
||||||
"""use localname for local users, username for remote"""
|
|
||||||
return user.localname if user.localname else user.username
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="notification_count")
|
|
||||||
def get_notification_count(user):
|
|
||||||
"""how many UNREAD notifications are there"""
|
|
||||||
return user.notification_set.filter(read=False).count()
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="replies")
|
|
||||||
def get_replies(status):
|
|
||||||
"""get all direct replies to a status"""
|
|
||||||
# TODO: this limit could cause problems
|
|
||||||
return models.Status.objects.filter(
|
|
||||||
reply_parent=status,
|
|
||||||
deleted=False,
|
|
||||||
).select_subclasses()[:10]
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="parent")
|
|
||||||
def get_parent(status):
|
|
||||||
"""get the reply parent for a status"""
|
|
||||||
return (
|
|
||||||
models.Status.objects.filter(id=status.reply_parent_id)
|
|
||||||
.select_subclasses()
|
|
||||||
.get()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="liked")
|
|
||||||
def get_user_liked(user, status):
|
|
||||||
"""did the given user fav a status?"""
|
|
||||||
try:
|
|
||||||
models.Favorite.objects.get(user=user, status=status)
|
|
||||||
return True
|
|
||||||
except models.Favorite.DoesNotExist:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="boosted")
|
|
||||||
def get_user_boosted(user, status):
|
|
||||||
"""did the given user fav a status?"""
|
|
||||||
return user.id in status.boosters.all().values_list("user", flat=True)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="follow_request_exists")
|
|
||||||
def follow_request_exists(user, requester):
|
|
||||||
"""see if there is a pending follow request for a user"""
|
|
||||||
try:
|
|
||||||
models.UserFollowRequest.objects.filter(
|
|
||||||
user_subject=requester,
|
|
||||||
user_object=user,
|
|
||||||
).get()
|
|
||||||
return True
|
|
||||||
except models.UserFollowRequest.DoesNotExist:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="boosted_status")
|
|
||||||
def get_boosted(boost):
|
|
||||||
"""load a boosted status. have to do this or it wont get foregin keys"""
|
|
||||||
return (
|
|
||||||
models.Status.objects.select_subclasses()
|
|
||||||
.filter(id=boost.boosted_status.id)
|
|
||||||
.get()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="book_description")
|
@register.filter(name="book_description")
|
||||||
def get_book_description(book):
|
def get_book_description(book):
|
||||||
"""use the work's text if the book doesn't have it"""
|
"""use the work's text if the book doesn't have it"""
|
||||||
return book.description or book.parent_work.description
|
return book.description or book.parent_work.description
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="uuid")
|
|
||||||
def get_uuid(identifier):
|
|
||||||
"""for avoiding clashing ids when there are many forms"""
|
|
||||||
return "%s%s" % (identifier, uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="to_markdown")
|
|
||||||
def get_markdown(content):
|
|
||||||
"""convert markdown to html"""
|
|
||||||
if content:
|
|
||||||
return to_markdown(content)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="mentions")
|
|
||||||
def get_mentions(status, user):
|
|
||||||
"""people to @ in a reply: the parent and all mentions"""
|
|
||||||
mentions = set([status.user] + list(status.mention_users.all()))
|
|
||||||
return (
|
|
||||||
" ".join("@" + get_user_identifier(m) for m in mentions if not m == user) + " "
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="status_preview_name")
|
|
||||||
def get_status_preview_name(obj):
|
|
||||||
"""text snippet with book context for a status"""
|
|
||||||
name = obj.__class__.__name__.lower()
|
|
||||||
if name == "review":
|
|
||||||
return "%s of <em>%s</em>" % (name, obj.book.title)
|
|
||||||
if name == "comment":
|
|
||||||
return "%s on <em>%s</em>" % (name, obj.book.title)
|
|
||||||
if name == "quotation":
|
|
||||||
return "%s from <em>%s</em>" % (name, obj.book.title)
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="next_shelf")
|
@register.filter(name="next_shelf")
|
||||||
def get_next_shelf(current_shelf):
|
def get_next_shelf(current_shelf):
|
||||||
"""shelf you'd use to update reading progress"""
|
"""shelf you'd use to update reading progress"""
|
||||||
|
@ -168,17 +54,6 @@ def get_next_shelf(current_shelf):
|
||||||
return "to-read"
|
return "to-read"
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="title")
|
|
||||||
def get_title(book):
|
|
||||||
"""display the subtitle if the title is short"""
|
|
||||||
if not book:
|
|
||||||
return ""
|
|
||||||
title = book.title
|
|
||||||
if len(title) < 6 and book.subtitle:
|
|
||||||
title = "{:s}: {:s}".format(title, book.subtitle)
|
|
||||||
return title
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
@register.simple_tag(takes_context=False)
|
||||||
def related_status(notification):
|
def related_status(notification):
|
||||||
"""for notifications"""
|
"""for notifications"""
|
||||||
|
@ -212,31 +87,6 @@ def latest_read_through(book, user):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
|
||||||
def active_read_through(book, user):
|
|
||||||
"""the most recent read activity"""
|
|
||||||
return (
|
|
||||||
models.ReadThrough.objects.filter(
|
|
||||||
user=user, book=book, finish_date__isnull=True
|
|
||||||
)
|
|
||||||
.order_by("-start_date")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
|
||||||
def comparison_bool(str1, str2):
|
|
||||||
"""idk why I need to write a tag for this, it reutrns a bool"""
|
|
||||||
return str1 == str2
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
|
||||||
def get_lang():
|
|
||||||
"""get current language, strip to the first two letters"""
|
|
||||||
language = utils.translation.get_language()
|
|
||||||
return language[0 : language.find("-")]
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def mutuals_count(context, user):
|
def mutuals_count(context, user):
|
||||||
"""how many users that you follow, follow them"""
|
"""how many users that you follow, follow them"""
|
||||||
|
|
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"""
|
"""barebones connector for search with defaults"""
|
||||||
self.assertIsInstance(self.connector.book_mappings, list)
|
self.assertIsInstance(self.connector.book_mappings, list)
|
||||||
|
|
||||||
def test_is_available(self):
|
|
||||||
"""this isn't used...."""
|
|
||||||
self.assertTrue(self.connector.is_available())
|
|
||||||
self.connector.max_query_count = 1
|
|
||||||
self.connector.connector.query_count = 2
|
|
||||||
self.assertFalse(self.connector.is_available())
|
|
||||||
|
|
||||||
def test_get_or_create_book_existing(self):
|
def test_get_or_create_book_existing(self):
|
||||||
"""find an existing book by remote/origin id"""
|
"""find an existing book by remote/origin id"""
|
||||||
self.assertEqual(models.Book.objects.count(), 1)
|
self.assertEqual(models.Book.objects.count(), 1)
|
||||||
|
|
|
@ -53,7 +53,6 @@ class AbstractConnector(TestCase):
|
||||||
self.assertEqual(connector.isbn_search_url, "https://example.com/isbn?q=")
|
self.assertEqual(connector.isbn_search_url, "https://example.com/isbn?q=")
|
||||||
self.assertIsNone(connector.name)
|
self.assertIsNone(connector.name)
|
||||||
self.assertEqual(connector.identifier, "example.com")
|
self.assertEqual(connector.identifier, "example.com")
|
||||||
self.assertIsNone(connector.max_query_count)
|
|
||||||
self.assertFalse(connector.local)
|
self.assertFalse(connector.local)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
|
|
|
@ -64,7 +64,7 @@ class Inventaire(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
formatted.cover,
|
formatted.cover,
|
||||||
"https://covers.inventaire.io/img/entities/ddb32e115a28dcc0465023869ba19f6868ec4042",
|
"https://covers.inventaire.io/img/entities/ddb32",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_cover_url(self):
|
def test_get_cover_url(self):
|
||||||
|
@ -74,18 +74,18 @@ class Inventaire(TestCase):
|
||||||
self.assertEqual(result, "https://covers.inventaire.io/img/entities/d46a8")
|
self.assertEqual(result, "https://covers.inventaire.io/img/entities/d46a8")
|
||||||
|
|
||||||
cover_blob = {
|
cover_blob = {
|
||||||
"url": "https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000",
|
"url": "https://commons.wikimedia.org/wiki/d.jpg?width=1000",
|
||||||
"file": "The Moonstone 1st ed.jpg",
|
"file": "The Moonstone 1st ed.jpg",
|
||||||
"credits": {
|
"credits": {
|
||||||
"text": "Wikimedia Commons",
|
"text": "Wikimedia Commons",
|
||||||
"url": "https://commons.wikimedia.org/wiki/File:The Moonstone 1st ed.jpg",
|
"url": "https://commons.wikimedia.org/wiki/File:The Moonstone.jpg",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result = self.connector.get_cover_url(cover_blob)
|
result = self.connector.get_cover_url(cover_blob)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result,
|
result,
|
||||||
"https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000",
|
"https://commons.wikimedia.org/wiki/d.jpg?width=1000",
|
||||||
)
|
)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"label": "The Stories of Vladimir Nabokov",
|
"label": "The Stories of Vladimir Nabokov",
|
||||||
"description": "book by Vladimir Nabokov",
|
"description": "book by Vladimir Nabokov",
|
||||||
"image": [
|
"image": [
|
||||||
"ddb32e115a28dcc0465023869ba19f6868ec4042"
|
"ddb32"
|
||||||
],
|
],
|
||||||
"_score": 25.180836,
|
"_score": 25.180836,
|
||||||
"_popularity": 4
|
"_popularity": 4
|
||||||
|
|
|
@ -2,12 +2,17 @@
|
||||||
import re
|
import re
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.templatetags import bookwyrm_tags
|
from bookwyrm.templatetags import (
|
||||||
|
bookwyrm_tags,
|
||||||
|
interaction,
|
||||||
|
markdown,
|
||||||
|
status_display,
|
||||||
|
utilities,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
||||||
|
@ -33,12 +38,6 @@ class TemplateTags(TestCase):
|
||||||
)
|
)
|
||||||
self.book = models.Edition.objects.create(title="Test Book")
|
self.book = models.Edition.objects.create(title="Test Book")
|
||||||
|
|
||||||
def test_dict_key(self, _):
|
|
||||||
"""just getting a value out of a dict"""
|
|
||||||
test_dict = {"a": 1, "b": 3}
|
|
||||||
self.assertEqual(bookwyrm_tags.dict_key(test_dict, "a"), 1)
|
|
||||||
self.assertEqual(bookwyrm_tags.dict_key(test_dict, "c"), 0)
|
|
||||||
|
|
||||||
def test_get_user_rating(self, _):
|
def test_get_user_rating(self, _):
|
||||||
"""get a user's most recent rating of a book"""
|
"""get a user's most recent rating of a book"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
@ -52,27 +51,14 @@ class TemplateTags(TestCase):
|
||||||
def test_get_user_identifer_local(self, _):
|
def test_get_user_identifer_local(self, _):
|
||||||
"""fall back to the simplest uid available"""
|
"""fall back to the simplest uid available"""
|
||||||
self.assertNotEqual(self.user.username, self.user.localname)
|
self.assertNotEqual(self.user.username, self.user.localname)
|
||||||
self.assertEqual(bookwyrm_tags.get_user_identifier(self.user), "mouse")
|
self.assertEqual(utilities.get_user_identifier(self.user), "mouse")
|
||||||
|
|
||||||
def test_get_user_identifer_remote(self, _):
|
def test_get_user_identifer_remote(self, _):
|
||||||
"""for a remote user, should be their full username"""
|
"""for a remote user, should be their full username"""
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bookwyrm_tags.get_user_identifier(self.remote_user), "rat@example.com"
|
utilities.get_user_identifier(self.remote_user), "rat@example.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_notification_count(self, _):
|
|
||||||
"""just countin'"""
|
|
||||||
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0)
|
|
||||||
|
|
||||||
models.Notification.objects.create(user=self.user, notification_type="FAVORITE")
|
|
||||||
models.Notification.objects.create(user=self.user, notification_type="MENTION")
|
|
||||||
|
|
||||||
models.Notification.objects.create(
|
|
||||||
user=self.remote_user, notification_type="FOLLOW"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 2)
|
|
||||||
|
|
||||||
def test_get_replies(self, _):
|
def test_get_replies(self, _):
|
||||||
"""direct replies to a status"""
|
"""direct replies to a status"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
@ -95,7 +81,7 @@ class TemplateTags(TestCase):
|
||||||
deleted_date=timezone.now(),
|
deleted_date=timezone.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
replies = bookwyrm_tags.get_replies(parent)
|
replies = status_display.get_replies(parent)
|
||||||
self.assertEqual(len(replies), 2)
|
self.assertEqual(len(replies), 2)
|
||||||
self.assertTrue(first_child in replies)
|
self.assertTrue(first_child in replies)
|
||||||
self.assertTrue(second_child in replies)
|
self.assertTrue(second_child in replies)
|
||||||
|
@ -111,7 +97,7 @@ class TemplateTags(TestCase):
|
||||||
reply_parent=parent, user=self.user, content="hi"
|
reply_parent=parent, user=self.user, content="hi"
|
||||||
)
|
)
|
||||||
|
|
||||||
result = bookwyrm_tags.get_parent(child)
|
result = status_display.get_parent(child)
|
||||||
self.assertEqual(result, parent)
|
self.assertEqual(result, parent)
|
||||||
self.assertIsInstance(result, models.Review)
|
self.assertIsInstance(result, models.Review)
|
||||||
|
|
||||||
|
@ -119,44 +105,26 @@ class TemplateTags(TestCase):
|
||||||
"""did a user like a status"""
|
"""did a user like a status"""
|
||||||
status = models.Review.objects.create(user=self.remote_user, book=self.book)
|
status = models.Review.objects.create(user=self.remote_user, book=self.book)
|
||||||
|
|
||||||
self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status))
|
self.assertFalse(interaction.get_user_liked(self.user, status))
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
models.Favorite.objects.create(user=self.user, status=status)
|
models.Favorite.objects.create(user=self.user, status=status)
|
||||||
self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status))
|
self.assertTrue(interaction.get_user_liked(self.user, status))
|
||||||
|
|
||||||
def test_get_user_boosted(self, _):
|
def test_get_user_boosted(self, _):
|
||||||
"""did a user boost a status"""
|
"""did a user boost a status"""
|
||||||
status = models.Review.objects.create(user=self.remote_user, book=self.book)
|
status = models.Review.objects.create(user=self.remote_user, book=self.book)
|
||||||
|
|
||||||
self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status))
|
self.assertFalse(interaction.get_user_boosted(self.user, status))
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
models.Boost.objects.create(user=self.user, boosted_status=status)
|
models.Boost.objects.create(user=self.user, boosted_status=status)
|
||||||
self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status))
|
self.assertTrue(interaction.get_user_boosted(self.user, status))
|
||||||
|
|
||||||
def test_follow_request_exists(self, _):
|
|
||||||
"""does a user want to follow"""
|
|
||||||
self.assertFalse(
|
|
||||||
bookwyrm_tags.follow_request_exists(self.user, self.remote_user)
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
models.UserFollowRequest.objects.create(
|
|
||||||
user_subject=self.user, user_object=self.remote_user
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertFalse(
|
|
||||||
bookwyrm_tags.follow_request_exists(self.user, self.remote_user)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
bookwyrm_tags.follow_request_exists(self.remote_user, self.user)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_boosted(self, _):
|
def test_get_boosted(self, _):
|
||||||
"""load a boosted status"""
|
"""load a boosted status"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
status = models.Review.objects.create(user=self.remote_user, book=self.book)
|
status = models.Review.objects.create(user=self.remote_user, book=self.book)
|
||||||
boost = models.Boost.objects.create(user=self.user, boosted_status=status)
|
boost = models.Boost.objects.create(user=self.user, boosted_status=status)
|
||||||
boosted = bookwyrm_tags.get_boosted(boost)
|
boosted = status_display.get_boosted(boost)
|
||||||
self.assertIsInstance(boosted, models.Review)
|
self.assertIsInstance(boosted, models.Review)
|
||||||
self.assertEqual(boosted, status)
|
self.assertEqual(boosted, status)
|
||||||
|
|
||||||
|
@ -178,48 +146,23 @@ class TemplateTags(TestCase):
|
||||||
|
|
||||||
def test_get_uuid(self, _):
|
def test_get_uuid(self, _):
|
||||||
"""uuid functionality"""
|
"""uuid functionality"""
|
||||||
uuid = bookwyrm_tags.get_uuid("hi")
|
uuid = utilities.get_uuid("hi")
|
||||||
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
|
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
|
||||||
|
|
||||||
def test_get_markdown(self, _):
|
def test_get_markdown(self, _):
|
||||||
"""mardown format data"""
|
"""mardown format data"""
|
||||||
result = bookwyrm_tags.get_markdown("_hi_")
|
result = markdown.get_markdown("_hi_")
|
||||||
self.assertEqual(result, "<p><em>hi</em></p>")
|
self.assertEqual(result, "<p><em>hi</em></p>")
|
||||||
|
|
||||||
result = bookwyrm_tags.get_markdown("<marquee>_hi_</marquee>")
|
result = markdown.get_markdown("<marquee>_hi_</marquee>")
|
||||||
self.assertEqual(result, "<p><em>hi</em></p>")
|
self.assertEqual(result, "<p><em>hi</em></p>")
|
||||||
|
|
||||||
def test_get_mentions(self, _):
|
def test_get_mentions(self, _):
|
||||||
"""list of people mentioned"""
|
"""list of people mentioned"""
|
||||||
status = models.Status.objects.create(content="hi", user=self.remote_user)
|
status = models.Status.objects.create(content="hi", user=self.remote_user)
|
||||||
result = bookwyrm_tags.get_mentions(status, self.user)
|
result = status_display.get_mentions(status, self.user)
|
||||||
self.assertEqual(result, "@rat@example.com ")
|
self.assertEqual(result, "@rat@example.com ")
|
||||||
|
|
||||||
def test_get_status_preview_name(self, _):
|
|
||||||
"""status context string"""
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
status = models.Status.objects.create(content="hi", user=self.user)
|
|
||||||
result = bookwyrm_tags.get_status_preview_name(status)
|
|
||||||
self.assertEqual(result, "status")
|
|
||||||
|
|
||||||
status = models.Review.objects.create(
|
|
||||||
content="hi", user=self.user, book=self.book
|
|
||||||
)
|
|
||||||
result = bookwyrm_tags.get_status_preview_name(status)
|
|
||||||
self.assertEqual(result, "review of <em>Test Book</em>")
|
|
||||||
|
|
||||||
status = models.Comment.objects.create(
|
|
||||||
content="hi", user=self.user, book=self.book
|
|
||||||
)
|
|
||||||
result = bookwyrm_tags.get_status_preview_name(status)
|
|
||||||
self.assertEqual(result, "comment on <em>Test Book</em>")
|
|
||||||
|
|
||||||
status = models.Quotation.objects.create(
|
|
||||||
content="hi", user=self.user, book=self.book
|
|
||||||
)
|
|
||||||
result = bookwyrm_tags.get_status_preview_name(status)
|
|
||||||
self.assertEqual(result, "quotation from <em>Test Book</em>")
|
|
||||||
|
|
||||||
def test_related_status(self, _):
|
def test_related_status(self, _):
|
||||||
"""gets the subclass model for a notification status"""
|
"""gets the subclass model for a notification status"""
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
|
|
@ -59,7 +59,6 @@ class BookViews(TestCase):
|
||||||
result.render()
|
result.render()
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
request = self.factory.get("")
|
|
||||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||||
is_api.return_value = True
|
is_api.return_value = True
|
||||||
result = view(request, self.book.id)
|
result = view(request, self.book.id)
|
||||||
|
|
|
@ -60,7 +60,12 @@ class FederationViews(TestCase):
|
||||||
|
|
||||||
def test_server_page_block(self):
|
def test_server_page_block(self):
|
||||||
"""block a server"""
|
"""block a server"""
|
||||||
server = models.FederatedServer.objects.create(server_name="hi.there.com")
|
server = models.FederatedServer.objects.create(
|
||||||
|
server_name="hi.there.com", application_type="bookwyrm"
|
||||||
|
)
|
||||||
|
connector = models.Connector.objects.get(
|
||||||
|
identifier="hi.there.com",
|
||||||
|
)
|
||||||
self.remote_user.federated_server = server
|
self.remote_user.federated_server = server
|
||||||
self.remote_user.save()
|
self.remote_user.save()
|
||||||
|
|
||||||
|
@ -72,17 +77,32 @@ class FederationViews(TestCase):
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
|
||||||
view(request, server.id)
|
view(request, server.id)
|
||||||
|
|
||||||
server.refresh_from_db()
|
server.refresh_from_db()
|
||||||
self.remote_user.refresh_from_db()
|
self.remote_user.refresh_from_db()
|
||||||
self.assertEqual(server.status, "blocked")
|
self.assertEqual(server.status, "blocked")
|
||||||
|
|
||||||
# and the user was deactivated
|
# and the user was deactivated
|
||||||
self.assertFalse(self.remote_user.is_active)
|
self.assertFalse(self.remote_user.is_active)
|
||||||
|
self.assertEqual(self.remote_user.deactivation_reason, "domain_block")
|
||||||
|
|
||||||
|
# and the connector was disabled
|
||||||
|
connector.refresh_from_db()
|
||||||
|
self.assertFalse(connector.active)
|
||||||
|
self.assertEqual(connector.deactivation_reason, "domain_block")
|
||||||
|
|
||||||
def test_server_page_unblock(self):
|
def test_server_page_unblock(self):
|
||||||
"""unblock a server"""
|
"""unblock a server"""
|
||||||
server = models.FederatedServer.objects.create(
|
server = models.FederatedServer.objects.create(
|
||||||
server_name="hi.there.com", status="blocked"
|
server_name="hi.there.com", status="blocked", application_type="bookwyrm"
|
||||||
)
|
)
|
||||||
|
connector = models.Connector.objects.get(
|
||||||
|
identifier="hi.there.com",
|
||||||
|
)
|
||||||
|
connector.active = False
|
||||||
|
connector.deactivation_reason = "domain_block"
|
||||||
|
connector.save()
|
||||||
|
|
||||||
self.remote_user.federated_server = server
|
self.remote_user.federated_server = server
|
||||||
self.remote_user.is_active = False
|
self.remote_user.is_active = False
|
||||||
self.remote_user.deactivation_reason = "domain_block"
|
self.remote_user.deactivation_reason = "domain_block"
|
||||||
|
@ -96,8 +116,15 @@ class FederationViews(TestCase):
|
||||||
server.refresh_from_db()
|
server.refresh_from_db()
|
||||||
self.remote_user.refresh_from_db()
|
self.remote_user.refresh_from_db()
|
||||||
self.assertEqual(server.status, "federated")
|
self.assertEqual(server.status, "federated")
|
||||||
|
|
||||||
# and the user was re-activated
|
# and the user was re-activated
|
||||||
self.assertTrue(self.remote_user.is_active)
|
self.assertTrue(self.remote_user.is_active)
|
||||||
|
self.assertIsNone(self.remote_user.deactivation_reason)
|
||||||
|
|
||||||
|
# and the connector was re-enabled
|
||||||
|
connector.refresh_from_db()
|
||||||
|
self.assertTrue(connector.active)
|
||||||
|
self.assertIsNone(connector.deactivation_reason)
|
||||||
|
|
||||||
def test_add_view_get(self):
|
def test_add_view_get(self):
|
||||||
"""there are so many views, this just makes sure it LOADS"""
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
|
|
@ -122,6 +122,14 @@ class ListViews(TestCase):
|
||||||
view = views.List.as_view()
|
view = views.List.as_view()
|
||||||
request = self.factory.get("")
|
request = self.factory.get("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
approved=True,
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
is_api.return_value = False
|
is_api.return_value = False
|
||||||
|
@ -130,6 +138,81 @@ class ListViews(TestCase):
|
||||||
result.render()
|
result.render()
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_list_page_sorted(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
view = views.List.as_view()
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
for (i, book) in enumerate([self.book, self.book_two, self.book_three]):
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=book,
|
||||||
|
approved=True,
|
||||||
|
order=i + 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.factory.get("/?sort_by=order")
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.list.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get("/?sort_by=title")
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.list.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get("/?sort_by=rating")
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.list.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get("/?sort_by=sdkfh")
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.list.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_list_page_empty(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
view = views.List.as_view()
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.list.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_list_page_logged_out(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
view = views.List.as_view()
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
approved=True,
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.factory.get("")
|
||||||
request.user = self.anonymous_user
|
request.user = self.anonymous_user
|
||||||
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
is_api.return_value = False
|
is_api.return_value = False
|
||||||
|
@ -138,12 +221,32 @@ class ListViews(TestCase):
|
||||||
result.render()
|
result.render()
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_list_page_json_view(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
view = views.List.as_view()
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
approved=True,
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
is_api.return_value = True
|
is_api.return_value = True
|
||||||
result = view(request, self.list.id)
|
result = view(request, self.list.id)
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_list_page_json_view_page(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
view = views.List.as_view()
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
request = self.factory.get("/?page=1")
|
request = self.factory.get("/?page=1")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
|
@ -204,466 +307,34 @@ class ListViews(TestCase):
|
||||||
result = view(request, self.list.id)
|
result = view(request, self.list.id)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
|
||||||
def test_curate_approve(self):
|
def test_user_lists_page(self):
|
||||||
"""approve a pending item"""
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
view = views.Curate.as_view()
|
view = views.UserLists.as_view()
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
pending = models.ListItem.objects.create(
|
models.List.objects.create(name="Public list", user=self.local_user)
|
||||||
book_list=self.list,
|
models.List.objects.create(
|
||||||
user=self.local_user,
|
name="Private list", privacy="direct", user=self.local_user
|
||||||
book=self.book,
|
|
||||||
approved=False,
|
|
||||||
order=1,
|
|
||||||
)
|
)
|
||||||
|
request = self.factory.get("")
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"item": pending.id,
|
|
||||||
"approved": "true",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
result = view(request, self.local_user.localname)
|
||||||
view(request, self.list.id)
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
self.assertEqual(mock.call_count, 2)
|
def test_user_lists_page_logged_out(self):
|
||||||
activity = json.loads(mock.call_args[0][1])
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
self.assertEqual(activity["type"], "Add")
|
view = views.UserLists.as_view()
|
||||||
self.assertEqual(activity["actor"], self.local_user.remote_id)
|
|
||||||
self.assertEqual(activity["target"], self.list.remote_id)
|
|
||||||
|
|
||||||
pending.refresh_from_db()
|
|
||||||
self.assertEqual(self.list.books.count(), 1)
|
|
||||||
self.assertEqual(self.list.listitem_set.first(), pending)
|
|
||||||
self.assertTrue(pending.approved)
|
|
||||||
|
|
||||||
def test_curate_reject(self):
|
|
||||||
"""approve a pending item"""
|
|
||||||
view = views.Curate.as_view()
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
pending = models.ListItem.objects.create(
|
models.List.objects.create(name="Public list", user=self.local_user)
|
||||||
book_list=self.list,
|
models.List.objects.create(
|
||||||
user=self.local_user,
|
name="Private list", privacy="direct", user=self.local_user
|
||||||
book=self.book,
|
|
||||||
approved=False,
|
|
||||||
order=1,
|
|
||||||
)
|
)
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.anonymous_user
|
||||||
|
|
||||||
request = self.factory.post(
|
result = view(request, self.local_user.username)
|
||||||
"",
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
{
|
result.render()
|
||||||
"item": pending.id,
|
self.assertEqual(result.status_code, 200)
|
||||||
"approved": "false",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
view(request, self.list.id)
|
|
||||||
|
|
||||||
self.assertFalse(self.list.books.exists())
|
|
||||||
self.assertFalse(models.ListItem.objects.exists())
|
|
||||||
|
|
||||||
def test_add_book(self):
|
|
||||||
"""put a book on a list"""
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
|
||||||
views.list.add_book(request)
|
|
||||||
self.assertEqual(mock.call_count, 1)
|
|
||||||
activity = json.loads(mock.call_args[0][1])
|
|
||||||
self.assertEqual(activity["type"], "Add")
|
|
||||||
self.assertEqual(activity["actor"], self.local_user.remote_id)
|
|
||||||
self.assertEqual(activity["target"], self.list.remote_id)
|
|
||||||
|
|
||||||
item = self.list.listitem_set.get()
|
|
||||||
self.assertEqual(item.book, self.book)
|
|
||||||
self.assertEqual(item.user, self.local_user)
|
|
||||||
self.assertTrue(item.approved)
|
|
||||||
|
|
||||||
def test_add_two_books(self):
|
|
||||||
"""
|
|
||||||
Putting two books on the list. The first should have an order value of
|
|
||||||
1 and the second should have an order value of 2.
|
|
||||||
"""
|
|
||||||
request_one = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request_one.user = self.local_user
|
|
||||||
|
|
||||||
request_two = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book_two.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request_two.user = self.local_user
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
views.list.add_book(request_one)
|
|
||||||
views.list.add_book(request_two)
|
|
||||||
|
|
||||||
items = self.list.listitem_set.order_by("order").all()
|
|
||||||
self.assertEqual(items[0].book, self.book)
|
|
||||||
self.assertEqual(items[1].book, self.book_two)
|
|
||||||
self.assertEqual(items[0].order, 1)
|
|
||||||
self.assertEqual(items[1].order, 2)
|
|
||||||
|
|
||||||
def test_add_three_books_and_remove_second(self):
|
|
||||||
"""
|
|
||||||
Put three books on a list and then remove the one in the middle. The
|
|
||||||
ordering of the list should adjust to not have a gap.
|
|
||||||
"""
|
|
||||||
request_one = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request_one.user = self.local_user
|
|
||||||
|
|
||||||
request_two = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book_two.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request_two.user = self.local_user
|
|
||||||
|
|
||||||
request_three = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book_three.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request_three.user = self.local_user
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
views.list.add_book(request_one)
|
|
||||||
views.list.add_book(request_two)
|
|
||||||
views.list.add_book(request_three)
|
|
||||||
|
|
||||||
items = self.list.listitem_set.order_by("order").all()
|
|
||||||
self.assertEqual(items[0].book, self.book)
|
|
||||||
self.assertEqual(items[1].book, self.book_two)
|
|
||||||
self.assertEqual(items[2].book, self.book_three)
|
|
||||||
self.assertEqual(items[0].order, 1)
|
|
||||||
self.assertEqual(items[1].order, 2)
|
|
||||||
self.assertEqual(items[2].order, 3)
|
|
||||||
|
|
||||||
remove_request = self.factory.post("", {"item": items[1].id})
|
|
||||||
remove_request.user = self.local_user
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
views.list.remove_book(remove_request, self.list.id)
|
|
||||||
items = self.list.listitem_set.order_by("order").all()
|
|
||||||
self.assertEqual(items[0].book, self.book)
|
|
||||||
self.assertEqual(items[1].book, self.book_three)
|
|
||||||
self.assertEqual(items[0].order, 1)
|
|
||||||
self.assertEqual(items[1].order, 2)
|
|
||||||
|
|
||||||
def test_adding_book_with_a_pending_book(self):
|
|
||||||
"""
|
|
||||||
When a list contains any pending books, the pending books should have
|
|
||||||
be at the end of the list by order. If a book is added while a book is
|
|
||||||
pending, its order should precede the pending books.
|
|
||||||
"""
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book_three.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
models.ListItem.objects.create(
|
|
||||||
book_list=self.list,
|
|
||||||
user=self.local_user,
|
|
||||||
book=self.book,
|
|
||||||
approved=True,
|
|
||||||
order=1,
|
|
||||||
)
|
|
||||||
models.ListItem.objects.create(
|
|
||||||
book_list=self.list,
|
|
||||||
user=self.rat,
|
|
||||||
book=self.book_two,
|
|
||||||
approved=False,
|
|
||||||
order=2,
|
|
||||||
)
|
|
||||||
views.list.add_book(request)
|
|
||||||
|
|
||||||
items = self.list.listitem_set.order_by("order").all()
|
|
||||||
self.assertEqual(items[0].book, self.book)
|
|
||||||
self.assertEqual(items[0].order, 1)
|
|
||||||
self.assertTrue(items[0].approved)
|
|
||||||
|
|
||||||
self.assertEqual(items[1].book, self.book_three)
|
|
||||||
self.assertEqual(items[1].order, 2)
|
|
||||||
self.assertTrue(items[1].approved)
|
|
||||||
|
|
||||||
self.assertEqual(items[2].book, self.book_two)
|
|
||||||
self.assertEqual(items[2].order, 3)
|
|
||||||
self.assertFalse(items[2].approved)
|
|
||||||
|
|
||||||
def test_approving_one_pending_book_from_multiple(self):
|
|
||||||
"""
|
|
||||||
When a list contains any pending books, the pending books should have
|
|
||||||
be at the end of the list by order. If a pending book is approved, then
|
|
||||||
its order should be at the end of the approved books and before the
|
|
||||||
remaining pending books.
|
|
||||||
"""
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
models.ListItem.objects.create(
|
|
||||||
book_list=self.list,
|
|
||||||
user=self.local_user,
|
|
||||||
book=self.book,
|
|
||||||
approved=True,
|
|
||||||
order=1,
|
|
||||||
)
|
|
||||||
models.ListItem.objects.create(
|
|
||||||
book_list=self.list,
|
|
||||||
user=self.local_user,
|
|
||||||
book=self.book_two,
|
|
||||||
approved=True,
|
|
||||||
order=2,
|
|
||||||
)
|
|
||||||
models.ListItem.objects.create(
|
|
||||||
book_list=self.list,
|
|
||||||
user=self.rat,
|
|
||||||
book=self.book_three,
|
|
||||||
approved=False,
|
|
||||||
order=3,
|
|
||||||
)
|
|
||||||
to_be_approved = models.ListItem.objects.create(
|
|
||||||
book_list=self.list,
|
|
||||||
user=self.rat,
|
|
||||||
book=self.book_four,
|
|
||||||
approved=False,
|
|
||||||
order=4,
|
|
||||||
)
|
|
||||||
|
|
||||||
view = views.Curate.as_view()
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"item": to_be_approved.id,
|
|
||||||
"approved": "true",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
view(request, self.list.id)
|
|
||||||
|
|
||||||
items = self.list.listitem_set.order_by("order").all()
|
|
||||||
self.assertEqual(items[0].book, self.book)
|
|
||||||
self.assertEqual(items[0].order, 1)
|
|
||||||
self.assertTrue(items[0].approved)
|
|
||||||
|
|
||||||
self.assertEqual(items[1].book, self.book_two)
|
|
||||||
self.assertEqual(items[1].order, 2)
|
|
||||||
self.assertTrue(items[1].approved)
|
|
||||||
|
|
||||||
self.assertEqual(items[2].book, self.book_four)
|
|
||||||
self.assertEqual(items[2].order, 3)
|
|
||||||
self.assertTrue(items[2].approved)
|
|
||||||
|
|
||||||
self.assertEqual(items[3].book, self.book_three)
|
|
||||||
self.assertEqual(items[3].order, 4)
|
|
||||||
self.assertFalse(items[3].approved)
|
|
||||||
|
|
||||||
def test_add_three_books_and_move_last_to_first(self):
|
|
||||||
"""
|
|
||||||
Put three books on the list and move the last book to the first
|
|
||||||
position.
|
|
||||||
"""
|
|
||||||
request_one = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request_one.user = self.local_user
|
|
||||||
|
|
||||||
request_two = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book_two.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request_two.user = self.local_user
|
|
||||||
|
|
||||||
request_three = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book_three.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request_three.user = self.local_user
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
views.list.add_book(request_one)
|
|
||||||
views.list.add_book(request_two)
|
|
||||||
views.list.add_book(request_three)
|
|
||||||
|
|
||||||
items = self.list.listitem_set.order_by("order").all()
|
|
||||||
self.assertEqual(items[0].book, self.book)
|
|
||||||
self.assertEqual(items[1].book, self.book_two)
|
|
||||||
self.assertEqual(items[2].book, self.book_three)
|
|
||||||
self.assertEqual(items[0].order, 1)
|
|
||||||
self.assertEqual(items[1].order, 2)
|
|
||||||
self.assertEqual(items[2].order, 3)
|
|
||||||
|
|
||||||
set_position_request = self.factory.post("", {"position": 1})
|
|
||||||
set_position_request.user = self.local_user
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
views.list.set_book_position(set_position_request, items[2].id)
|
|
||||||
items = self.list.listitem_set.order_by("order").all()
|
|
||||||
self.assertEqual(items[0].book, self.book_three)
|
|
||||||
self.assertEqual(items[1].book, self.book)
|
|
||||||
self.assertEqual(items[2].book, self.book_two)
|
|
||||||
self.assertEqual(items[0].order, 1)
|
|
||||||
self.assertEqual(items[1].order, 2)
|
|
||||||
self.assertEqual(items[2].order, 3)
|
|
||||||
|
|
||||||
def test_add_book_outsider(self):
|
|
||||||
"""put a book on a list"""
|
|
||||||
self.list.curation = "open"
|
|
||||||
self.list.save(broadcast=False)
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.rat
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
|
||||||
views.list.add_book(request)
|
|
||||||
self.assertEqual(mock.call_count, 1)
|
|
||||||
activity = json.loads(mock.call_args[0][1])
|
|
||||||
self.assertEqual(activity["type"], "Add")
|
|
||||||
self.assertEqual(activity["actor"], self.rat.remote_id)
|
|
||||||
self.assertEqual(activity["target"], self.list.remote_id)
|
|
||||||
|
|
||||||
item = self.list.listitem_set.get()
|
|
||||||
self.assertEqual(item.book, self.book)
|
|
||||||
self.assertEqual(item.user, self.rat)
|
|
||||||
self.assertTrue(item.approved)
|
|
||||||
|
|
||||||
def test_add_book_pending(self):
|
|
||||||
"""put a book on a list awaiting approval"""
|
|
||||||
self.list.curation = "curated"
|
|
||||||
self.list.save(broadcast=False)
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.rat
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
|
||||||
views.list.add_book(request)
|
|
||||||
|
|
||||||
self.assertEqual(mock.call_count, 1)
|
|
||||||
activity = json.loads(mock.call_args[0][1])
|
|
||||||
|
|
||||||
self.assertEqual(activity["type"], "Add")
|
|
||||||
self.assertEqual(activity["actor"], self.rat.remote_id)
|
|
||||||
self.assertEqual(activity["target"], self.list.remote_id)
|
|
||||||
|
|
||||||
item = self.list.listitem_set.get()
|
|
||||||
self.assertEqual(activity["object"]["id"], item.remote_id)
|
|
||||||
|
|
||||||
self.assertEqual(item.book, self.book)
|
|
||||||
self.assertEqual(item.user, self.rat)
|
|
||||||
self.assertFalse(item.approved)
|
|
||||||
|
|
||||||
def test_add_book_self_curated(self):
|
|
||||||
"""put a book on a list automatically approved"""
|
|
||||||
self.list.curation = "curated"
|
|
||||||
self.list.save(broadcast=False)
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"book": self.book.id,
|
|
||||||
"list": self.list.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
|
|
||||||
views.list.add_book(request)
|
|
||||||
self.assertEqual(mock.call_count, 1)
|
|
||||||
activity = json.loads(mock.call_args[0][1])
|
|
||||||
self.assertEqual(activity["type"], "Add")
|
|
||||||
self.assertEqual(activity["actor"], self.local_user.remote_id)
|
|
||||||
self.assertEqual(activity["target"], self.list.remote_id)
|
|
||||||
|
|
||||||
item = self.list.listitem_set.get()
|
|
||||||
self.assertEqual(item.book, self.book)
|
|
||||||
self.assertEqual(item.user, self.local_user)
|
|
||||||
self.assertTrue(item.approved)
|
|
||||||
|
|
||||||
def test_remove_book(self):
|
|
||||||
"""take an item off a list"""
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
item = models.ListItem.objects.create(
|
|
||||||
book_list=self.list,
|
|
||||||
user=self.local_user,
|
|
||||||
book=self.book,
|
|
||||||
order=1,
|
|
||||||
)
|
|
||||||
self.assertTrue(self.list.listitem_set.exists())
|
|
||||||
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"item": item.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
views.list.remove_book(request, self.list.id)
|
|
||||||
self.assertFalse(self.list.listitem_set.exists())
|
|
||||||
|
|
||||||
def test_remove_book_unauthorized(self):
|
|
||||||
"""take an item off a list"""
|
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
|
||||||
item = models.ListItem.objects.create(
|
|
||||||
book_list=self.list, user=self.local_user, book=self.book, order=1
|
|
||||||
)
|
|
||||||
self.assertTrue(self.list.listitem_set.exists())
|
|
||||||
request = self.factory.post(
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"item": item.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.user = self.rat
|
|
||||||
|
|
||||||
views.list.remove_book(request, self.list.id)
|
|
||||||
self.assertTrue(self.list.listitem_set.exists())
|
|
||||||
|
|
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,
|
"author": author,
|
||||||
"books": [b.default_edition for b in books],
|
"books": [b.default_edition for b in books],
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "author.html", data)
|
return TemplateResponse(request, "author/author.html", data)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
|
@ -43,7 +43,7 @@ class EditAuthor(View):
|
||||||
"""info about a book"""
|
"""info about a book"""
|
||||||
author = get_object_or_404(models.Author, id=author_id)
|
author = get_object_or_404(models.Author, id=author_id)
|
||||||
data = {"author": author, "form": forms.AuthorForm(instance=author)}
|
data = {"author": author, "form": forms.AuthorForm(instance=author)}
|
||||||
return TemplateResponse(request, "edit_author.html", data)
|
return TemplateResponse(request, "author/edit_author.html", data)
|
||||||
|
|
||||||
def post(self, request, author_id):
|
def post(self, request, author_id):
|
||||||
"""edit a author cool"""
|
"""edit a author cool"""
|
||||||
|
@ -52,7 +52,7 @@ class EditAuthor(View):
|
||||||
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
|
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
data = {"author": author, "form": form}
|
data = {"author": author, "form": form}
|
||||||
return TemplateResponse(request, "edit_author.html", data)
|
return TemplateResponse(request, "author/edit_author.html", data)
|
||||||
author = form.save()
|
author = form.save()
|
||||||
|
|
||||||
return redirect("/author/%s" % author.id)
|
return redirect("/author/%s" % author.id)
|
||||||
|
|
|
@ -30,6 +30,7 @@ class Book(View):
|
||||||
|
|
||||||
def get(self, request, book_id, user_statuses=False):
|
def get(self, request, book_id, user_statuses=False):
|
||||||
"""info about a book"""
|
"""info about a book"""
|
||||||
|
user_statuses = user_statuses if request.user.is_authenticated else False
|
||||||
try:
|
try:
|
||||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||||
except models.Book.DoesNotExist:
|
except models.Book.DoesNotExist:
|
||||||
|
@ -51,9 +52,9 @@ class Book(View):
|
||||||
)
|
)
|
||||||
|
|
||||||
# the reviews to show
|
# the reviews to show
|
||||||
if user_statuses and request.user.is_authenticated:
|
if user_statuses:
|
||||||
if user_statuses == "review":
|
if user_statuses == "review":
|
||||||
queryset = book.review_set
|
queryset = book.review_set.select_subclasses()
|
||||||
elif user_statuses == "comment":
|
elif user_statuses == "comment":
|
||||||
queryset = book.comment_set
|
queryset = book.comment_set
|
||||||
else:
|
else:
|
||||||
|
@ -67,7 +68,9 @@ class Book(View):
|
||||||
"book": book,
|
"book": book,
|
||||||
"statuses": paginated.get_page(request.GET.get("page")),
|
"statuses": paginated.get_page(request.GET.get("page")),
|
||||||
"review_count": reviews.count(),
|
"review_count": reviews.count(),
|
||||||
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")),
|
"ratings": reviews.filter(Q(content__isnull=True) | Q(content=""))
|
||||||
|
if not user_statuses
|
||||||
|
else None,
|
||||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||||
"lists": privacy_filter(
|
"lists": privacy_filter(
|
||||||
request.user, book.list_set.filter(listitem__approved=True)
|
request.user, book.list_set.filter(listitem__approved=True)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from urllib.parse import urlencode
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
from django.db.models import Avg, Count, Q, Max
|
from django.db.models import Avg, Count, DecimalField, Q, Max
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse
|
from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
@ -108,31 +108,23 @@ class List(View):
|
||||||
if direction not in ("ascending", "descending"):
|
if direction not in ("ascending", "descending"):
|
||||||
direction = "ascending"
|
direction = "ascending"
|
||||||
|
|
||||||
internal_sort_by = {
|
directional_sort_by = {
|
||||||
"order": "order",
|
"order": "order",
|
||||||
"title": "book__title",
|
"title": "book__title",
|
||||||
"rating": "average_rating",
|
"rating": "average_rating",
|
||||||
}
|
}[sort_by]
|
||||||
directional_sort_by = internal_sort_by[sort_by]
|
|
||||||
if direction == "descending":
|
if direction == "descending":
|
||||||
directional_sort_by = "-" + directional_sort_by
|
directional_sort_by = "-" + directional_sort_by
|
||||||
|
|
||||||
if sort_by == "order":
|
items = book_list.listitem_set
|
||||||
items = book_list.listitem_set.filter(approved=True).order_by(
|
if sort_by == "rating":
|
||||||
directional_sort_by
|
items = items.annotate(
|
||||||
)
|
average_rating=Avg(
|
||||||
elif sort_by == "title":
|
Coalesce("book__review__rating", 0.0),
|
||||||
items = book_list.listitem_set.filter(approved=True).order_by(
|
output_field=DecimalField(),
|
||||||
directional_sort_by
|
|
||||||
)
|
|
||||||
elif sort_by == "rating":
|
|
||||||
items = (
|
|
||||||
book_list.listitem_set.annotate(
|
|
||||||
average_rating=Avg(Coalesce("book__review__rating", 0))
|
|
||||||
)
|
)
|
||||||
.filter(approved=True)
|
|
||||||
.order_by(directional_sort_by)
|
|
||||||
)
|
)
|
||||||
|
items = items.filter(approved=True).order_by(directional_sort_by)
|
||||||
|
|
||||||
paginated = Paginator(items, PAGE_LENGTH)
|
paginated = Paginator(items, PAGE_LENGTH)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
from django.db.models import Count, OuterRef, Subquery, F, Q
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
|
@ -37,30 +38,41 @@ class Shelf(View):
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
if not shelf.visible_to_user(request.user):
|
if not shelf.visible_to_user(request.user):
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
books = shelf.books
|
||||||
# this is a constructed "all books" view, with a fake "shelf" obj
|
# this is a constructed "all books" view, with a fake "shelf" obj
|
||||||
else:
|
else:
|
||||||
FakeShelf = namedtuple(
|
FakeShelf = namedtuple(
|
||||||
"Shelf", ("identifier", "name", "user", "books", "privacy")
|
"Shelf", ("identifier", "name", "user", "books", "privacy")
|
||||||
)
|
)
|
||||||
books = models.Edition.objects.filter(
|
books = models.Edition.objects.filter(
|
||||||
|
# privacy is ensured because the shelves are already filtered above
|
||||||
shelfbook__shelf__in=shelves.all()
|
shelfbook__shelf__in=shelves.all()
|
||||||
).distinct()
|
).distinct()
|
||||||
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
||||||
|
|
||||||
is_self = request.user == user
|
|
||||||
|
|
||||||
if is_api_request(request):
|
if is_api_request(request):
|
||||||
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
||||||
|
|
||||||
|
reviews = privacy_filter(
|
||||||
|
request.user,
|
||||||
|
models.Review.objects.filter(
|
||||||
|
user=user,
|
||||||
|
rating__isnull=False,
|
||||||
|
book__id=OuterRef("id"),
|
||||||
|
),
|
||||||
|
).order_by("-published_date")
|
||||||
|
|
||||||
|
books = books.annotate(rating=Subquery(reviews.values("rating")[:1]))
|
||||||
|
|
||||||
paginated = Paginator(
|
paginated = Paginator(
|
||||||
shelf.books.order_by("-updated_date"),
|
books.order_by("-updated_date"),
|
||||||
PAGE_LENGTH,
|
PAGE_LENGTH,
|
||||||
)
|
)
|
||||||
|
|
||||||
page = paginated.get_page(request.GET.get("page"))
|
page = paginated.get_page(request.GET.get("page"))
|
||||||
data = {
|
data = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"is_self": is_self,
|
"is_self": request.user == user,
|
||||||
"shelves": shelves.all(),
|
"shelves": shelves.all(),
|
||||||
"shelf": shelf,
|
"shelf": shelf,
|
||||||
"books": page,
|
"books": page,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue