Merge branch 'main' into top-bar

This commit is contained in:
Mouse Reeve 2021-05-18 11:39:36 -07:00 committed by GitHub
commit 9f789cd5d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 2895 additions and 2479 deletions

View file

@ -74,6 +74,14 @@ class Connector(AbstractConnector):
**{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks"]},
}
def search(self, query, min_confidence=None):
"""overrides default search function with confidence ranking"""
results = super().search(query)
if min_confidence:
# filter the search results after the fact
return [r for r in results if r.confidence >= min_confidence]
return results
def parse_search_data(self, data):
return data.get("results")
@ -84,6 +92,9 @@ class Connector(AbstractConnector):
if images
else None
)
# a deeply messy translation of inventaire's scores
confidence = float(search_result.get("_score", 0.1))
confidence = 0.1 if confidence < 150 else 0.999
return SearchResult(
title=search_result.get("label"),
key=self.get_remote_id(search_result.get("uri")),
@ -92,6 +103,7 @@ class Connector(AbstractConnector):
self.base_url, search_result.get("uri")
),
cover=cover,
confidence=confidence,
connector=self,
)

View file

@ -3,3 +3,4 @@
from .importer import Importer
from .goodreads_import import GoodreadsImporter
from .librarything_import import LibrarythingImporter
from .storygraph_import import StorygraphImporter

View file

@ -0,0 +1,34 @@
""" handle reading a csv from librarything """
import re
import math
from . import Importer
class StorygraphImporter(Importer):
"""csv downloads from librarything"""
service = "Storygraph"
# mandatory_fields : fields matching the book title and author
mandatory_fields = ["Title"]
def parse_fields(self, entry):
"""custom parsing for storygraph"""
data = {}
data["import_source"] = self.service
data["Title"] = entry["Title"]
data["Author"] = entry["Authors"] if "Authors" in entry else entry["Author"]
data["ISBN13"] = entry["ISBN"]
data["My Review"] = entry["Review"]
if entry["Star Rating"]:
data["My Rating"] = math.ceil(float(entry["Star Rating"]))
else:
data["My Rating"] = ""
data["Date Added"] = re.sub(r"[/]", "-", entry["Date Added"])
data["Date Read"] = re.sub(r"[/]", "-", entry["Last Date Read"])
data["Exclusive Shelf"] = (
{"read": "read", "currently-reading": "reading", "to-read": "to-read"}
).get(entry["Read Status"], None)
return data

View file

@ -128,7 +128,9 @@ class ImportItem(models.Model):
@property
def rating(self):
"""x/5 star rating for a book"""
return int(self.data["My Rating"])
if self.data.get("My Rating", None):
return int(self.data["My Rating"])
return None
@property
def date_added(self):

View file

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

View file

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

View file

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

View file

@ -38,9 +38,8 @@
{% if user_authenticated and can_edit_book %}
<div class="column is-narrow">
<a href="{{ book.id }}/edit">
<span class="icon icon-pencil" title="{% trans "Edit Book" %}">
<span class="is-sr-only">{% trans "Edit Book" %}</span>
</span>
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
<span>{% trans "Edit Book" %}</span>
</a>
</div>
{% endif %}
@ -163,12 +162,9 @@
</div>
<div class="column is-narrow">
{% trans "Add read dates" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="plus" class="is-small" controls_text="add-readthrough" %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add-readthrough" focus="add-readthrough-focus" %}
</div>
</header>
{% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
<section class="is-hidden box" id="add-readthrough">
<form name="add-readthrough" action="/create-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=None %}
@ -183,6 +179,9 @@
</div>
</form>
</section>
{% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
{% for readthrough in readthroughs %}
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
{% endfor %}
@ -257,7 +256,7 @@
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
<a href="{{ rating.remote_id }}">{{ rating.published_date|naturaltime }}</a>
</div>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,9 @@
<option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}>
GoodReads (CSV)
</option>
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
Storygraph (CSV)
</option>
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
LibraryThing (TSV)
</option>

View file

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

View file

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

View file

@ -1,12 +1,13 @@
{% extends 'lists/list_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load markdown %}
{% block panel %}
{% if request.user == list.user and pending_count %}
<div class="block content">
<p>
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count | pluralize }} awaiting your approval</a>
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count|pluralize }} awaiting your approval</a>
</p>
</div>
{% endif %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %}
{% load markdown %}
<div class="columns is-multiline">
{% for list in lists %}
<div class="column is-one-quarter">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,7 @@
{% load bookwyrm_tags %}
{% load utilities %}
{% load status_display %}
{% load i18n %}
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{% if type == 'direct' %}status{% else %}{{ type }}{% endif %}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}">
{% csrf_token %}
@ -100,7 +103,7 @@
{# 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">
<div class="columns mt-1">
<div class="field has-addons column">
<div class="control">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,12 @@ from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import forms, models
from bookwyrm.importers import Importer, LibrarythingImporter, GoodreadsImporter
from bookwyrm.importers import (
Importer,
LibrarythingImporter,
GoodreadsImporter,
StorygraphImporter,
)
from bookwyrm.tasks import app
# pylint: disable= no-self-use
@ -42,6 +47,8 @@ class Import(View):
importer = None
if source == "LibraryThing":
importer = LibrarythingImporter()
elif source == "Storygraph":
importer = StorygraphImporter()
else:
# Default : GoodReads
importer = GoodreadsImporter()

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff