Merge pull request #1477 from bookwyrm-social/add-edit-book

Updates for adding and editing books
This commit is contained in:
Mouse Reeve 2021-10-01 10:36:09 -07:00 committed by GitHub
commit 7d03bfd2f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 777 additions and 784 deletions

View file

@ -24,5 +24,5 @@ jobs:
--rule 'meta_viewport: true' \
--rule 'no_autofocus: true' \
--rule 'tabindex_no_positive: true' \
--exclude '_modal.html|create_status/layout.html' \
--exclude '_modal.html|create_status/layout.html|reading_modals/layout.html' \
bookwyrm/templates

View file

@ -203,7 +203,9 @@
<hr aria-hidden="true">
<section class="box">
{% with 0|uuid as controls_uid %}
{% include 'snippets/create_status.html' with book=book hide_cover=True %}
{% endwith %}
</section>
{% endif %}
<div class="block" id="reviews">

View file

@ -1,6 +1,7 @@
{% spaceless %}
{% load i18n %}
{% if book.isbn13 or book.oclc_number or book.asin %}
<dl>
{% if book.isbn_13 %}
<div class="is-flex">
@ -23,4 +24,5 @@
</div>
{% endif %}
</dl>
{% endif %}
{% endspaceless %}

View file

@ -0,0 +1,116 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
{% block content %}
<header class="block">
<h1 class="title level-left">
{% if book %}
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
{% else %}
{% trans "Add Book" %}
{% endif %}
</h1>
{% if book %}
<dl>
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Updated:" %}</dt>
<dd class="ml-2">{{ book.updated_date | naturaltime }}</dd>
{% if book.last_edited_by %}
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Last edited by:" %}</dt>
<dd class="ml-2"><a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></dd>
{% endif %}
</dl>
{% endif %}
</header>
<form
class="block"
{% if book %}
name="edit-book"
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
{% else %}
name="create-book"
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
{% endif %}
method="post"
enctype="multipart/form-data"
>
{% if confirm_mode %}
<div class="box">
<h2 class="title is-4">{% trans "Confirm Book Info" %}</h2>
<div class="columns mb-4">
{% if author_matches %}
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
<div class="column is-half">
{% for author in author_matches %}
<fieldset>
<legend class="title is-5 mb-1">
{% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}
</legend>
{% with forloop.counter0 as counter %}
{% for match in author.matches %}
<label class="label mb-2">
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
{{ match.name }}
</label>
<p class="help">
<a href="{{ match.local_path }}" target="_blank">{% blocktrans with book_title=match.book_set.first.title %}Author of <em>{{ book_title }}</em>{% endblocktrans %}</a>
</p>
{% endfor %}
<label class="label">
<input type="radio" name="author_match-{{ counter }}" value="{{ author.name }}" required> {% trans "This is a new author" %}
</label>
{% endwith %}
</fieldset>
{% endfor %}
</div>
{% else %}
<p class="column is-half">{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}</p>
{% endif %}
{% if not book %}
<div class="column is-half">
<fieldset>
<legend class="title is-5 mb-1">
{% trans "Is this an edition of an existing work?" %}
</legend>
{% for match in book_matches %}
<label class="label">
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
</label>
{% endfor %}
<label>
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
</label>
</fieldset>
</div>
{% endif %}
</div>
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
<a href="#" class="button" data-back>
<span>{% trans "Back" %}</span>
</a>
</div>
<hr class="block">
{% endif %}
{% include "book/edit/edit_book_form.html" %}
{% if not confirm_mode %}
<div class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
</div>
{% endif %}
</form>
{% endblock %}

View file

@ -1,40 +1,4 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
{% block content %}
<header class="block">
<h1 class="title level-left">
{% if book %}
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
{% else %}
{% trans "Add Book" %}
{% endif %}
</h1>
{% if book %}
<dl>
<div class="is-flex">
<dt class="has-text-weight-semibold">{% trans "Added:" %}</dt>
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
</div>
<div class="is-flex">
<dt class="has-text-weight-semibold">{% trans "Updated:" %}</dt>
<dd class="ml-2">{{ book.updated_date | naturaltime }}</dd>
</div>
{% if book.last_edited_by %}
<div class="is-flex">
<dt class="has-text-weight-semibold">{% trans "Last edited by:" %}</dt>
<dd class="ml-2"><a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></dd>
</div>
{% endif %}
</dl>
{% endif %}
</header>
{% if form.non_field_errors %}
<div class="block">
@ -42,87 +6,14 @@
</div>
{% endif %}
<form
class="block"
{% if book %}
name="edit-book"
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
{% else %}
name="create-book"
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
{% endif %}
method="post"
enctype="multipart/form-data"
>
{% csrf_token %}
{% if confirm_mode %}
<div class="box">
<h2 class="title is-4">{% trans "Confirm Book Info" %}</h2>
<div class="columns mb-4">
{% if author_matches %}
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
<div class="column is-half">
{% for author in author_matches %}
<fieldset>
<legend class="title is-5 mb-1">
{% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}
</legend>
{% with forloop.counter0 as counter %}
{% for match in author.matches %}
<label class="label mb-2">
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
{{ match.name }}
</label>
<p class="help">
<a href="{{ match.local_path }}" target="_blank">{% blocktrans with book_title=match.book_set.first.title %}Author of <em>{{ book_title }}</em>{% endblocktrans %}</a>
</p>
{% endfor %}
<label class="label">
<input type="radio" name="author_match-{{ counter }}" value="{{ author.name }}" required> {% trans "This is a new author" %}
</label>
{% endwith %}
</fieldset>
{% endfor %}
</div>
{% else %}
<p class="column is-half">{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}</p>
{% endif %}
{% if not book %}
<div class="column is-half">
<fieldset>
<legend class="title is-5 mb-1">
{% trans "Is this an edition of an existing work?" %}
</legend>
{% for match in book_matches %}
<label class="label">
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
</label>
{% endfor %}
<label>
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
</label>
</fieldset>
</div>
{% endif %}
</div>
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
<a href="#" class="button" data-back>
<span>{% trans "Back" %}</span>
</a>
</div>
<hr class="block">
{% endif %}
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<div class="columns">
<div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2>
{% csrf_token %}
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<div class="columns">
<div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2>
<div class="box">
<div class="field">
<label class="label" for="id_title">{% trans "Title:" %}</label>
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
@ -147,20 +38,25 @@
{% endfor %}
</div>
<div class="field">
<label class="label" for="id_series">{% trans "Series:" %}</label>
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
{% for error in form.series.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="field">
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
{{ form.series_number }}
{% for error in form.series_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<div class="columns">
<div class="column is-two-thirds">
<div class="field">
<label class="label" for="id_series">{% trans "Series:" %}</label>
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
{% for error in form.series.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
<div class="column is-one-third">
<div class="field">
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
{{ form.series_number }}
{% for error in form.series_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</div>
<div class="field">
@ -171,7 +67,12 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</section>
<section class="block">
<h2 class="title is-4">{% trans "Publication" %}</h2>
<div class="box">
<div class="field">
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
{{ form.publishers }}
@ -196,10 +97,12 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</section>
</div>
</section>
<section class="block">
<h2 class="title is-4">{% trans "Authors" %}</h2>
<section class="block">
<h2 class="title is-4">{% trans "Authors" %}</h2>
<div class="box">
{% if book.authors.exists %}
<fieldset>
{% for author in book.authors.all %}
@ -220,18 +123,22 @@
<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 %}>
<span class="help">{% trans "Separate multiple values with commas." %}</span>
</div>
</section>
</div>
</div>
</section>
</div>
<div class="column is-half">
<div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Cover" %}</h2>
<div class="columns">
<div class="column is-3 is-cover">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' size_mobile='xlarge' size='large' %}
</div>
<div class="box">
<div class="columns">
{% if book.cover %}
<div class="column is-3 is-cover">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' size_mobile='xlarge' size='large' %}
</div>
{% endif %}
<div class="column">
<div class="block">
<div class="column">
<div class="field">
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
{{ form.cover }}
@ -248,9 +155,11 @@
</div>
</div>
</div>
</section>
<div class="block">
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
<section class="block">
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
<div class="box">
<div class="columns">
<div class="column is-one-third">
<div class="field">
@ -282,9 +191,11 @@
{% endfor %}
</div>
</div>
</section>
<div class="block">
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
<section class="block">
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
<div class="box">
<div class="field">
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
{{ form.isbn_13 }}
@ -333,15 +244,6 @@
{% endfor %}
</div>
</div>
</div>
</section>
</div>
{% if not confirm_mode %}
<div class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
</div>
{% endif %}
</form>
{% endblock %}
</div>

View file

@ -1,7 +0,0 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'book/search_filter.html' %}
{% include 'book/language_filter.html' %}
{% include 'book/format_filter.html' %}
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'book/editions/search_filter.html' %}
{% include 'book/editions/language_filter.html' %}
{% include 'book/editions/format_filter.html' %}
{% endblock %}

View file

@ -8,7 +8,7 @@
<h1 class="title">{% blocktrans with work_path=work.local_path work_title=work|book_title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
</div>
{% include 'book/edition_filters.html' %}
{% include 'book/editions/edition_filters.html' %}
<div class="block">
{% for book in editions %}

View file

@ -3,31 +3,30 @@
{% load i18n %}
{% load humanize %}
{% firstof book.physical_format_detail book.physical_format as format %}
{% firstof book.physical_format book.physical_format_detail as format_property %}
{% with pages=book.pages %}
{% if format or pages %}
{% if format_property %}
<meta itemprop="bookFormat" content="{{ format_property }}">
{% endif %}
{% if pages %}
<meta itemprop="numberOfPages" content="{{ pages }}">
{% endif %}
<p>
{% firstof book.physical_format_detail book.physical_format as format %}
{% firstof book.physical_format book.physical_format_detail as format_property %}
{% with pages=book.pages %}
{% if format %}
{% comment %}
@todo The bookFormat property is limited to a list of values whereas the book edition is free text.
@see https://schema.org/bookFormat
{% endcomment %}
<meta itemprop="bookFormat" content="{{ format_property }}">
{% endif %}
{% if pages %}
<meta itemprop="numberOfPages" content="{{ pages }}">
{% endif %}
{% if format and not pages %}
{% blocktrans %}{{ format }}{% endblocktrans %}
{% elif format and pages %}
{% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %}
{% elif pages %}
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
{% endif %}
{% endwith %}
{% if format and not pages %}
{% blocktrans %}{{ format }}{% endblocktrans %}
{% elif format and pages %}
{% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %}
{% elif pages %}
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
{% endif %}
</p>
{% endif %}
{% endwith %}
{% if book.languages %}
{% for language in book.languages %}
@ -41,32 +40,34 @@
</p>
{% endif %}
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
{% if date or book.first_published_date or book.publishers %}
{% if date or book.first_published_date %}
<meta
itemprop="datePublished"
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
>
{% endif %}
<p>
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
{% if date or book.first_published_date %}
<meta
itemprop="datePublished"
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
>
{% endif %}
{% comment %}
@todo The publisher property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor.
@see https://schema.org/Publisher
{% endcomment %}
{% if book.publishers %}
{% for publisher in book.publishers %}
<meta itemprop="publisher" content="{{ publisher }}">
{% endfor %}
{% endif %}
{% comment %}
@todo The publisher property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor.
@see https://schema.org/Publisher
{% endcomment %}
{% if book.publishers %}
{% for publisher in book.publishers %}
<meta itemprop="publisher" content="{{ publisher }}">
{% endfor %}
{% endif %}
{% if date and publisher %}
{% blocktrans %}Published {{ date }} by {{ publisher }}.{% endblocktrans %}
{% elif date %}
{% blocktrans %}Published {{ date }}{% endblocktrans %}
{% elif publisher %}
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
{% endif %}
{% endwith %}
{% if date and publisher %}
{% blocktrans %}Published {{ date }} by {{ publisher }}.{% endblocktrans %}
{% elif date %}
{% blocktrans %}Published {{ date }}{% endblocktrans %}
{% elif publisher %}
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
{% endif %}
</p>
{% endif %}
{% endwith %}
{% endspaceless %}

View file

@ -12,7 +12,7 @@ draft: an existing Status object that is providing default values for input fiel
name="content"
class="textarea save-draft"
data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
placeholder="{{ placeholder }}"
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}

View file

@ -19,7 +19,7 @@ reply_parent: the Status object this post will be in reply to, if applicable
name="{{ type }}"
action="/post/{{ type }}"
method="post"
id="tab_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
id="form_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
>
{% endblock %}
@ -36,7 +36,7 @@ reply_parent: the Status object this post will be in reply to, if applicable
{# fields that go between the content warnings and the content field (ie, quote) #}
{% block pre_content_additions %}{% endblock %}
<label class="label" for="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}">
<label class="label" for="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}">
{% block content_label %}
{% trans "Comment:" %}
{% endblock %}

View file

@ -1,6 +1,10 @@
{% load i18n %}
<div class="content">
<p>{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}</p>
<p>
{% blocktrans trimmed %}
Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.
{% endblocktrans %}
</p>
<form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}">
{% csrf_token %}
@ -20,7 +24,7 @@
<div class="column">
<label class="label" for="privacy_{{ goal.id }}">{% trans "Goal privacy:" %}</label>
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy uuid=goal.id %}
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy privacy_uuid=goal.id %}
</div>
</div>

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% load utilities %}
<div class="select {{ class }}">
{% firstof uuid 0|uuid as uuid %}
{% firstof privacy_uuid 0|uuid as uuid %}
{% if not no_label %}
<label class="is-sr-only" for="privacy_{{ uuid }}">{% trans "Post privacy" %}</label>
{% endif %}

View file

@ -5,7 +5,7 @@
type="number"
name="progress"
class="input"
id="id_progress_{{ readthrough.id }}"
id="id_progress_{{ readthrough.id }}{{ controls_uid }}"
value="{{ readthrough.progress }}"
{% if progress_required %}required{% endif %}
>

View file

@ -35,3 +35,7 @@ Finish "<em>{{ book_title }}</em>"
</div>
</div>
{% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=True type="finish_modal" %}
{% endblock %}

View file

@ -15,3 +15,5 @@
<input type="hidden" name="mention_books" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ book.id }}">
{% endblock %}
{% block form_close %}{% endblock %}

View file

@ -23,10 +23,11 @@
<div id="reading_content_{{ local_uuid }}_{{ uuid }}">
<hr aria-hidden="true">
<fieldset id="reading_content_fieldset_{{ local_uuid }}_{{ uuid }}">
{% comparison_bool controls_text "progress_update" True as optional %}
{% include "snippets/reading_modals/form.html" with optional=optional %}
{% block form %}{% endblock %}
</fieldset>
</div>
{% endwith %}
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -12,6 +12,10 @@
{% endblock %}
{% block reading-dates %}
<label for="id_progress_{{ readthrough.id }}" class="label">{% trans "Progress:" %}</label>
<label for="id_progress_{{ readthrough.id }}{{ controls_uid }}" class="label">{% trans "Progress:" %}</label>
{% include "snippets/progress_field.html" with progress_required=True %}
{% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=False type="update_modal" %}
{% endblock %}

View file

@ -22,3 +22,7 @@ Start "<em>{{ book_title }}</em>"
<input type="date" name="start_date" class="input" id="start_id_start_date_{{ uuid }}" value="{% now "Y-m-d" %}">
</div>
{% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=True type="start_modal" %}
{% endblock %}

View file

@ -13,3 +13,7 @@ Want to Read "<em>{{ book_title }}</em>"
<input type="hidden" name="reading_status" value="to-read">
{% csrf_token %}
{% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=True type="want_modal" %}
{% endblock %}

View file

@ -10,7 +10,7 @@
</div>
{# Only show progress for editing existing readthroughs #}
{% if readthrough.id and not readthrough.finish_date %}
<label class="label" for="id_progress_{{ readthrough.id }}">
<label class="label" for="id_progress_{{ readthrough.id }}{{ controls_uid }}">
{% trans "Progress" %}
</label>
{% include "snippets/progress_field.html" %}

View file

@ -11,7 +11,7 @@
{% if status.user == request.user %}
{# things you can do to your own statuses #}
<li role="menuitem" class="dropdown-item p-0">
<form name="delete-{{ status.id }}" action="/delete-status/{{ status.id }}" method="post">
<form name="delete-{{ status.id|uuid }}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %}
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
{% trans "Delete status" %}

View file

@ -1,5 +1,6 @@
{% load utilities %}
{% if fallback_url %}
<form name="fallback-form-{{ controls_uuid}}" method="GET" action="{{ fallback_url }}">
<form name="fallback_form_{{ 0|uuid }}" method="GET" action="{{ fallback_url }}">
{% endif %}
<button
{% if not fallback_url %}

View file

@ -0,0 +1,21 @@
""" html validation on rendered templates """
from tidylib import tidy_document
def validate_html(html):
"""run tidy on html"""
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
# idk how else to filter out these unescape amp errs
errors = "\n".join(
e
for e in errors.split("\n")
if "&book" not in e and "id and name attribute" not in e
)
if errors:
raise Exception(errors)

View file

@ -1,11 +1,11 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class DashboardViews(TestCase):
@ -35,8 +35,5 @@ class DashboardViews(TestCase):
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)

View file

@ -1,12 +1,12 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class EmailBlocklistViews(TestCase):
@ -38,10 +38,7 @@ class EmailBlocklistViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_blocklist_page_post(self):
@ -54,10 +51,7 @@ class EmailBlocklistViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertTrue(

View file

@ -1,7 +1,6 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from tidylib import tidy_document
from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse
@ -9,6 +8,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
class FederationViews(TestCase):
@ -48,16 +48,7 @@ class FederationViews(TestCase):
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_instance_page(self):
@ -70,10 +61,7 @@ class FederationViews(TestCase):
result = view(request, server.id)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_server_page_block(self):
@ -162,10 +150,7 @@ class FederationViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_add_view_post_create(self):

View file

@ -1,11 +1,11 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class IPBlocklistViews(TestCase):
@ -37,8 +37,5 @@ class IPBlocklistViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)

View file

@ -1,13 +1,13 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
class ReportViews(TestCase):
@ -44,16 +44,7 @@ class ReportViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_reports_page_with_data(self):
@ -66,16 +57,7 @@ class ReportViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_report_page(self):
@ -89,10 +71,7 @@ class ReportViews(TestCase):
result = view(request, report.id)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_report_comment(self):

View file

@ -1,6 +1,5 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import Group
from django.template.response import TemplateResponse
@ -8,6 +7,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class UserAdminViews(TestCase):
@ -36,10 +36,7 @@ class UserAdminViews(TestCase):
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_user_admin_page(self):
@ -52,10 +49,7 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -77,10 +71,7 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(
list(self.local_user.groups.values_list("name", flat=True)), ["editor"]

View file

@ -0,0 +1 @@
from . import *

View file

@ -0,0 +1,228 @@
""" test for app action functionality """
from io import BytesIO
import pathlib
from unittest.mock import patch
from PIL import Image
import responses
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import Http404
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import timezone
from bookwyrm import forms, models, views
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html
class BookViews(TestCase):
"""books books books"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
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.group = Group.objects.create(name="editor")
self.group.permissions.add(
Permission.objects.create(
name="edit_book",
codename="edit_book",
content_type=ContentType.objects.get_for_model(models.User),
).id
)
self.work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=self.work,
)
models.SiteSettings.objects.create()
def test_book_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
models.ReadThrough.objects.create(
user=self.local_user,
book=self.book,
start_date=timezone.now(),
)
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.book.id)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
is_api.return_value = True
result = view(request, self.book.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.add_status_task.delay")
def test_book_page_statuses(self, *_):
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
review = models.Review.objects.create(
user=self.local_user,
book=self.book,
content="hi",
)
comment = models.Comment.objects.create(
user=self.local_user,
book=self.book,
content="hi",
)
quote = models.Quotation.objects.create(
user=self.local_user,
book=self.book,
content="hi",
quote="wow",
)
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.book.id, user_statuses="review")
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["statuses"].object_list[0], review)
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.book.id, user_statuses="comment")
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["statuses"].object_list[0], comment)
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.book.id, user_statuses="quotation")
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["statuses"].object_list[0], quote)
def test_book_page_invalid_id(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
is_api.return_value = False
with self.assertRaises(Http404):
view(request, 0)
def test_book_page_work_id(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["book"], self.book)
def test_upload_cover_file(self):
"""add a cover via file upload"""
self.assertFalse(self.book.cover)
image_file = pathlib.Path(__file__).parent.joinpath(
"../../../static/images/default_avi.jpg"
)
form = forms.CoverForm(instance=self.book)
# pylint: disable=consider-using-with
form.data["cover"] = SimpleUploadedFile(
image_file, open(image_file, "rb").read(), content_type="image/jpeg"
)
request = self.factory.post("", form.data)
request.user = self.local_user
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
) as delay_mock:
views.upload_cover(request, self.book.id)
self.assertEqual(delay_mock.call_count, 1)
self.book.refresh_from_db()
self.assertTrue(self.book.cover)
@responses.activate
def test_upload_cover_url(self):
"""add a cover via url"""
self.assertFalse(self.book.cover)
form = forms.CoverForm(instance=self.book)
form.data["cover-url"] = _setup_cover_url()
request = self.factory.post("", form.data)
request.user = self.local_user
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
) as delay_mock:
views.upload_cover(request, self.book.id)
self.assertEqual(delay_mock.call_count, 1)
self.book.refresh_from_db()
self.assertTrue(self.book.cover)
def test_add_description(self):
"""add a book description"""
self.local_user.groups.add(self.group)
request = self.factory.post("", {"description": "new description hi"})
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.add_description(request, self.book.id)
self.book.refresh_from_db()
self.assertEqual(self.book.description, "new description hi")
self.assertEqual(self.book.last_edited_by, self.local_user)
def _setup_cover_url():
"""creates cover url mock"""
cover_url = "http://example.com"
image_file = pathlib.Path(__file__).parent.joinpath(
"../../../static/images/default_avi.jpg"
)
image = Image.open(image_file)
output = BytesIO()
image.save(output, format=image.format)
responses.add(
responses.GET,
cover_url,
body=output.getvalue(),
status=200,
)
return cover_url

View file

@ -1,25 +1,19 @@
""" test for app action functionality """
from io import BytesIO
import pathlib
from unittest.mock import patch
from PIL import Image
import responses
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import Http404
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import timezone
from bookwyrm import forms, models, views
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html
from bookwyrm.tests.views.books.test_book import _setup_cover_url
class BookViews(TestCase):
class EditBookViews(TestCase):
"""books books books"""
def setUp(self):
@ -53,102 +47,6 @@ class BookViews(TestCase):
models.SiteSettings.objects.create()
def test_book_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
models.ReadThrough.objects.create(
user=self.local_user,
book=self.book,
start_date=timezone.now(),
)
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.book.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = True
result = view(request, self.book.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.add_status_task.delay")
def test_book_page_statuses(self, *_):
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
review = models.Review.objects.create(
user=self.local_user,
book=self.book,
content="hi",
)
comment = models.Comment.objects.create(
user=self.local_user,
book=self.book,
content="hi",
)
quote = models.Quotation.objects.create(
user=self.local_user,
book=self.book,
content="hi",
quote="wow",
)
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.book.id, user_statuses="review")
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["statuses"].object_list[0], review)
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.book.id, user_statuses="comment")
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["statuses"].object_list[0], comment)
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.book.id, user_statuses="quotation")
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["statuses"].object_list[0], quote)
def test_book_page_invalid_id(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False
with self.assertRaises(Http404):
view(request, 0)
def test_book_page_work_id(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["book"], self.book)
def test_edit_book_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.EditBook.as_view()
@ -157,7 +55,7 @@ class BookViews(TestCase):
request.user.is_superuser = True
result = view(request, self.book.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_edit_book(self):
@ -188,7 +86,7 @@ class BookViews(TestCase):
request.user = self.local_user
result = view(request, self.book.id)
result.render()
validate_html(result.render())
# the changes haven't been saved yet
self.book.refresh_from_db()
@ -283,29 +181,12 @@ class BookViews(TestCase):
self.assertEqual(book.authors.first().name, "Sappho")
self.assertEqual(book.authors.first(), book.parent_work.authors.first())
def _setup_cover_url(self):
cover_url = "http://example.com"
image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg"
)
image = Image.open(image_file)
output = BytesIO()
image.save(output, format=image.format)
responses.add(
responses.GET,
cover_url,
body=output.getvalue(),
status=200,
)
return cover_url
@responses.activate
def test_create_book_upload_cover_url(self):
"""create an entirely new book and work with cover url"""
self.assertFalse(self.book.cover)
view = views.ConfirmEditBook.as_view()
self.local_user.groups.add(self.group)
cover_url = self._setup_cover_url()
cover_url = _setup_cover_url()
form = forms.EditionForm()
form.data["title"] = "New Title"
@ -322,59 +203,3 @@ class BookViews(TestCase):
self.book.refresh_from_db()
self.assertTrue(self.book.cover)
def test_upload_cover_file(self):
"""add a cover via file upload"""
self.assertFalse(self.book.cover)
image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg"
)
form = forms.CoverForm(instance=self.book)
form.data["cover"] = SimpleUploadedFile(
image_file, open(image_file, "rb").read(), content_type="image/jpeg"
)
request = self.factory.post("", form.data)
request.user = self.local_user
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
) as delay_mock:
views.upload_cover(request, self.book.id)
self.assertEqual(delay_mock.call_count, 1)
self.book.refresh_from_db()
self.assertTrue(self.book.cover)
@responses.activate
def test_upload_cover_url(self):
"""add a cover via url"""
self.assertFalse(self.book.cover)
form = forms.CoverForm(instance=self.book)
form.data["cover-url"] = self._setup_cover_url()
request = self.factory.post("", form.data)
request.user = self.local_user
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
) as delay_mock:
views.upload_cover(request, self.book.id)
self.assertEqual(delay_mock.call_count, 1)
self.book.refresh_from_db()
self.assertTrue(self.book.cover)
def test_add_description(self):
"""add a book description"""
self.local_user.groups.add(self.group)
request = self.factory.post("", {"description": "new description hi"})
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.add_description(request, self.book.id)
self.book.refresh_from_db()
self.assertEqual(self.book.description, "new description hi")
self.assertEqual(self.book.last_edited_by, self.local_user)

View file

@ -7,6 +7,7 @@ from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html
class BookViews(TestCase):
@ -40,11 +41,11 @@ class BookViews(TestCase):
"""there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view()
request = self.factory.get("")
with patch("bookwyrm.views.editions.is_api_request") as is_api:
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertTrue("paperback" in result.context_data["formats"])
@ -57,11 +58,11 @@ class BookViews(TestCase):
)
view = views.Editions.as_view()
request = self.factory.get("")
with patch("bookwyrm.views.editions.is_api_request") as is_api:
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 2)
self.assertEqual(len(result.context_data["formats"]), 2)
@ -69,26 +70,26 @@ class BookViews(TestCase):
self.assertTrue("okay" in result.context_data["formats"])
request = self.factory.get("", {"q": "fish"})
with patch("bookwyrm.views.editions.is_api_request") as is_api:
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
result.render()
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1)
request = self.factory.get("", {"q": "okay"})
with patch("bookwyrm.views.editions.is_api_request") as is_api:
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
result.render()
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1)
request = self.factory.get("", {"format": "okay"})
with patch("bookwyrm.views.editions.is_api_request") as is_api:
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
is_api.return_value = False
result = view(request, self.work.id)
result.render()
validate_html(result.render())
self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1)
@ -96,7 +97,7 @@ class BookViews(TestCase):
"""there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view()
request = self.factory.get("")
with patch("bookwyrm.views.editions.is_api_request") as is_api:
with patch("bookwyrm.views.books.editions.is_api_request") as is_api:
is_api.return_value = True
result = view(request, self.work.id)
self.assertIsInstance(result, ActivitypubResponse)

View file

@ -1,12 +1,12 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@ -46,10 +46,7 @@ class BlockViews(TestCase):
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_block_post(self, _):

View file

@ -1,12 +1,12 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class ChangePasswordViews(TestCase):
@ -35,10 +35,7 @@ class ChangePasswordViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_password_change(self):

View file

@ -1,7 +1,6 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.sessions.middleware import SessionMiddleware
from django.template.response import TemplateResponse
@ -9,6 +8,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.suggested_users.remove_user_task.delay")
@ -53,10 +53,7 @@ class DeleteUserViews(TestCase):
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task")

View file

@ -2,7 +2,6 @@
import pathlib
from unittest.mock import patch
from PIL import Image
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser
from django.core.files.base import ContentFile
@ -12,6 +11,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.suggested_users.remove_user_task.delay")
@ -58,10 +58,7 @@ class EditUserViews(TestCase):
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_edit_user(self, _):

View file

@ -1,6 +1,5 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser
from django.template.response import TemplateResponse
@ -8,6 +7,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
# pylint: disable=unused-argument
class DirectoryViews(TestCase):
@ -52,16 +52,7 @@ class DirectoryViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_directory_page_empty(self):
@ -72,10 +63,7 @@ class DirectoryViews(TestCase):
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_directory_page_logged_out(self):

View file

@ -1,6 +1,5 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser
from django.http import Http404
@ -10,6 +9,7 @@ from django.test.client import RequestFactory
from django.utils import timezone
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class GoalViews(TestCase):
@ -62,16 +62,7 @@ class GoalViews(TestCase):
request.user = self.local_user
result = view(request, self.local_user.localname, self.year)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertIsInstance(result, TemplateResponse)
def test_goal_page_anonymous(self):
@ -102,16 +93,7 @@ class GoalViews(TestCase):
request.user = self.rat
result = view(request, self.local_user.localname, timezone.now().year)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertIsInstance(result, TemplateResponse)
def test_goal_page_private(self):

View file

@ -1,7 +1,6 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from tidylib import tidy_document
from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
@ -10,6 +9,7 @@ from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@ -56,16 +56,7 @@ class ShelfViews(TestCase):
is_api.return_value = False
result = view(request, self.local_user.username, shelf.identifier)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.shelf.is_api_request") as is_api:

View file

@ -1,6 +1,5 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser
from django.http.response import Http404
@ -10,6 +9,7 @@ from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html
class UserViews(TestCase):
@ -56,16 +56,7 @@ class UserViews(TestCase):
is_api.return_value = False
result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
request.user = self.anonymous_user
@ -73,16 +64,7 @@ class UserViews(TestCase):
is_api.return_value = False
result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.user.is_api_request") as is_api:
@ -111,16 +93,7 @@ class UserViews(TestCase):
is_api.return_value = False
result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.user.is_api_request") as is_api:
@ -151,16 +124,7 @@ class UserViews(TestCase):
is_api.return_value = False
result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.user.is_api_request") as is_api:

View file

@ -27,13 +27,15 @@ from .preferences.edit_user import EditUser
from .preferences.delete_user import DeleteUser
from .preferences.block import Block, unblock
# books
from .books.books import Book, upload_cover, add_description, resolve_book
from .books.edit_book import EditBook, ConfirmEditBook
from .books.editions import Editions, switch_edition
# misc views
from .author import Author, EditAuthor
from .books import Book, EditBook, ConfirmEditBook
from .books import upload_cover, add_description, resolve_book
from .directory import Directory
from .discover import Discover
from .editions import Editions, switch_edition
from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request

View file

View file

@ -0,0 +1,182 @@
""" the good stuff! the books! """
from uuid import uuid4
from django.contrib.auth.decorators import login_required, permission_required
from django.core.files.base import ContentFile
from django.core.paginator import Paginator
from django.db.models import Avg, Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request, privacy_filter
# pylint: disable=no-self-use
class Book(View):
"""a book! this is the stuff"""
def get(self, request, book_id, user_statuses=False):
"""info about a book"""
if is_api_request(request):
book = get_object_or_404(
models.Book.objects.select_subclasses(), id=book_id
)
return ActivitypubResponse(book.to_activity())
user_statuses = user_statuses if request.user.is_authenticated else False
# it's safe to use this OR because edition and work and subclasses of the same
# table, so they never have clashing IDs
book = (
models.Edition.viewer_aware_objects(request.user)
.filter(Q(id=book_id) | Q(parent_work__id=book_id))
.order_by("-edition_rank")
.select_related("parent_work")
.prefetch_related("authors")
.first()
)
if not book or not book.parent_work:
raise Http404()
# all reviews for all editions of the book
reviews = privacy_filter(
request.user, models.Review.objects.filter(book__parent_work__editions=book)
)
# the reviews to show
if user_statuses:
if user_statuses == "review":
queryset = book.review_set.select_subclasses()
elif user_statuses == "comment":
queryset = book.comment_set
else:
queryset = book.quotation_set
queryset = queryset.filter(user=request.user, deleted=False)
else:
queryset = reviews.exclude(Q(content__isnull=True) | Q(content=""))
queryset = queryset.select_related("user").order_by("-published_date")
paginated = Paginator(queryset, PAGE_LENGTH)
lists = privacy_filter(
request.user,
models.List.objects.filter(
listitem__approved=True,
listitem__book__in=book.parent_work.editions.all(),
),
)
data = {
"book": book,
"statuses": paginated.get_page(request.GET.get("page")),
"review_count": reviews.count(),
"ratings": reviews.filter(
Q(content__isnull=True) | Q(content="")
).select_related("user")
if not user_statuses
else None,
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": lists,
}
if request.user.is_authenticated:
readthroughs = models.ReadThrough.objects.filter(
user=request.user,
book=book,
).order_by("start_date")
for readthrough in readthroughs:
readthrough.progress_updates = (
readthrough.progressupdate_set.all().order_by("-updated_date")
)
data["readthroughs"] = readthroughs
data["user_shelfbooks"] = models.ShelfBook.objects.filter(
user=request.user, book=book
).select_related("shelf")
data["other_edition_shelves"] = models.ShelfBook.objects.filter(
~Q(book=book),
user=request.user,
book__parent_work=book.parent_work,
).select_related("shelf", "book")
filters = {"user": request.user, "deleted": False}
data["user_statuses"] = {
"review_count": book.review_set.filter(**filters).count(),
"comment_count": book.comment_set.filter(**filters).count(),
"quotation_count": book.quotation_set.filter(**filters).count(),
}
return TemplateResponse(request, "book/book.html", data)
@login_required
@require_POST
def upload_cover(request, book_id):
"""upload a new cover"""
book = get_object_or_404(models.Edition, id=book_id)
book.last_edited_by = request.user
url = request.POST.get("cover-url")
if url:
image = set_cover_from_url(url)
if image:
book.cover.save(*image)
return redirect(f"{book.local_path}?cover_error=True")
form = forms.CoverForm(request.POST, request.FILES, instance=book)
if not form.is_valid() or not form.files.get("cover"):
return redirect(book.local_path)
book.cover = form.files["cover"]
book.save()
return redirect(book.local_path)
def set_cover_from_url(url):
"""load it from a url"""
try:
image_file = get_image(url)
except: # pylint: disable=bare-except
return None
if not image_file:
return None
image_name = str(uuid4()) + "." + url.split(".")[-1]
image_content = ContentFile(image_file.content)
return [image_name, image_content]
@login_required
@require_POST
@permission_required("bookwyrm.edit_book", raise_exception=True)
def add_description(request, book_id):
"""upload a new cover"""
book = get_object_or_404(models.Edition, id=book_id)
description = request.POST.get("description")
book.description = description
book.last_edited_by = request.user
book.save(update_fields=["description", "last_edited_by"])
return redirect("book", book.id)
@require_POST
def resolve_book(request):
"""figure out the local path to a book from a remote_id"""
remote_id = request.POST.get("remote_id")
connector = connector_manager.get_or_create_connector(remote_id)
book = connector.get_or_create_book(remote_id)
return redirect("book", book.id)

View file

@ -1,128 +1,22 @@
""" the good stuff! the books! """
from uuid import uuid4
from dateutil.parser import parse as dateparse
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector
from django.core.files.base import ContentFile
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import Avg, Q
from django.http import HttpResponseBadRequest, Http404
from django.http import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_edition, privacy_filter
from bookwyrm.views.helpers import get_edition
from .books import set_cover_from_url
# pylint: disable=no-self-use
class Book(View):
"""a book! this is the stuff"""
def get(self, request, book_id, user_statuses=False):
"""info about a book"""
if is_api_request(request):
book = get_object_or_404(
models.Book.objects.select_subclasses(), id=book_id
)
return ActivitypubResponse(book.to_activity())
user_statuses = user_statuses if request.user.is_authenticated else False
# it's safe to use this OR because edition and work and subclasses of the same
# table, so they never have clashing IDs
book = (
models.Edition.viewer_aware_objects(request.user)
.filter(Q(id=book_id) | Q(parent_work__id=book_id))
.order_by("-edition_rank")
.select_related("parent_work")
.prefetch_related("authors")
.first()
)
if not book or not book.parent_work:
raise Http404()
# all reviews for all editions of the book
reviews = privacy_filter(
request.user, models.Review.objects.filter(book__parent_work__editions=book)
)
# the reviews to show
if user_statuses:
if user_statuses == "review":
queryset = book.review_set.select_subclasses()
elif user_statuses == "comment":
queryset = book.comment_set
else:
queryset = book.quotation_set
queryset = queryset.filter(user=request.user, deleted=False)
else:
queryset = reviews.exclude(Q(content__isnull=True) | Q(content=""))
queryset = queryset.select_related("user").order_by("-published_date")
paginated = Paginator(queryset, PAGE_LENGTH)
lists = privacy_filter(
request.user,
models.List.objects.filter(
listitem__approved=True,
listitem__book__in=book.parent_work.editions.all(),
),
)
data = {
"book": book,
"statuses": paginated.get_page(request.GET.get("page")),
"review_count": reviews.count(),
"ratings": reviews.filter(
Q(content__isnull=True) | Q(content="")
).select_related("user")
if not user_statuses
else None,
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": lists,
}
if request.user.is_authenticated:
readthroughs = models.ReadThrough.objects.filter(
user=request.user,
book=book,
).order_by("start_date")
for readthrough in readthroughs:
readthrough.progress_updates = (
readthrough.progressupdate_set.all().order_by("-updated_date")
)
data["readthroughs"] = readthroughs
data["user_shelfbooks"] = models.ShelfBook.objects.filter(
user=request.user, book=book
).select_related("shelf")
data["other_edition_shelves"] = models.ShelfBook.objects.filter(
~Q(book=book),
user=request.user,
book__parent_work=book.parent_work,
).select_related("shelf", "book")
filters = {"user": request.user, "deleted": False}
data["user_statuses"] = {
"review_count": book.review_set.filter(**filters).count(),
"comment_count": book.comment_set.filter(**filters).count(),
"quotation_count": book.quotation_set.filter(**filters).count(),
}
return TemplateResponse(request, "book/book.html", data)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
@ -138,7 +32,7 @@ class EditBook(View):
if not book.description:
book.description = book.parent_work.description
data = {"book": book, "form": forms.EditionForm(instance=book)}
return TemplateResponse(request, "book/edit_book.html", data)
return TemplateResponse(request, "book/edit/edit_book.html", data)
def post(self, request, book_id=None):
"""edit a book cool"""
@ -148,7 +42,7 @@ class EditBook(View):
data = {"book": book, "form": form}
if not form.is_valid():
return TemplateResponse(request, "book/edit_book.html", data)
return TemplateResponse(request, "book/edit/edit_book.html", data)
add_author = request.POST.get("add_author")
# we're adding an author through a free text field
@ -207,7 +101,7 @@ class EditBook(View):
except (MultiValueDictKeyError, ValueError):
pass
data["form"].data = formcopy
return TemplateResponse(request, "book/edit_book.html", data)
return TemplateResponse(request, "book/edit/edit_book.html", data)
remove_authors = request.POST.getlist("remove_authors")
for author_id in remove_authors:
@ -238,7 +132,7 @@ class ConfirmEditBook(View):
data = {"book": book, "form": form}
if not form.is_valid():
return TemplateResponse(request, "book/edit_book.html", data)
return TemplateResponse(request, "book/edit/edit_book.html", data)
with transaction.atomic():
# save book
@ -284,67 +178,3 @@ class ConfirmEditBook(View):
book.save(broadcast=False)
return redirect(f"/book/{book.id}")
@login_required
@require_POST
def upload_cover(request, book_id):
"""upload a new cover"""
book = get_object_or_404(models.Edition, id=book_id)
book.last_edited_by = request.user
url = request.POST.get("cover-url")
if url:
image = set_cover_from_url(url)
if image:
book.cover.save(*image)
return redirect(f"{book.local_path}?cover_error=True")
form = forms.CoverForm(request.POST, request.FILES, instance=book)
if not form.is_valid() or not form.files.get("cover"):
return redirect(book.local_path)
book.cover = form.files["cover"]
book.save()
return redirect(book.local_path)
def set_cover_from_url(url):
"""load it from a url"""
try:
image_file = get_image(url)
except: # pylint: disable=bare-except
return None
if not image_file:
return None
image_name = str(uuid4()) + "." + url.split(".")[-1]
image_content = ContentFile(image_file.content)
return [image_name, image_content]
@login_required
@require_POST
@permission_required("bookwyrm.edit_book", raise_exception=True)
def add_description(request, book_id):
"""upload a new cover"""
book = get_object_or_404(models.Edition, id=book_id)
description = request.POST.get("description")
book.description = description
book.last_edited_by = request.user
book.save(update_fields=["description", "last_edited_by"])
return redirect("book", book.id)
@require_POST
def resolve_book(request):
"""figure out the local path to a book from a remote_id"""
remote_id = request.POST.get("remote_id")
connector = connector_manager.get_or_create_connector(remote_id)
book = connector.get_or_create_book(remote_id)
return redirect("book", book.id)

View file

@ -14,7 +14,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request
from bookwyrm.views.helpers import is_api_request
# pylint: disable=no-self-use
@ -66,7 +66,7 @@ class Editions(View):
e.physical_format.lower() for e in editions if e.physical_format
),
}
return TemplateResponse(request, "book/editions.html", data)
return TemplateResponse(request, "book/editions/editions.html", data)
@login_required