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 'meta_viewport: true' \
--rule 'no_autofocus: true' \ --rule 'no_autofocus: true' \
--rule 'tabindex_no_positive: 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 bookwyrm/templates

View file

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

View file

@ -1,6 +1,7 @@
{% spaceless %} {% spaceless %}
{% load i18n %} {% load i18n %}
{% if book.isbn13 or book.oclc_number or book.asin %}
<dl> <dl>
{% if book.isbn_13 %} {% if book.isbn_13 %}
<div class="is-flex"> <div class="is-flex">
@ -23,4 +24,5 @@
</div> </div>
{% endif %} {% endif %}
</dl> </dl>
{% endif %}
{% endspaceless %} {% 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 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 %} {% if form.non_field_errors %}
<div class="block"> <div class="block">
@ -42,87 +6,14 @@
</div> </div>
{% endif %} {% endif %}
<form {% csrf_token %}
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 %} <input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
{% if confirm_mode %} <div class="columns">
<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"> <div class="column is-half">
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <h2 class="title is-4">{% trans "Metadata" %}</h2>
<div class="box">
<div class="field"> <div class="field">
<label class="label" for="id_title">{% trans "Title:" %}</label> <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"> <input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
@ -147,6 +38,8 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="columns">
<div class="column is-two-thirds">
<div class="field"> <div class="field">
<label class="label" for="id_series">{% trans "Series:" %}</label> <label class="label" for="id_series">{% trans "Series:" %}</label>
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}"> <input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
@ -154,7 +47,8 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
</div>
<div class="column is-one-third">
<div class="field"> <div class="field">
<label class="label" for="id_series_number">{% trans "Series number:" %}</label> <label class="label" for="id_series_number">{% trans "Series number:" %}</label>
{{ form.series_number }} {{ form.series_number }}
@ -162,6 +56,8 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
</div>
</div>
<div class="field"> <div class="field">
<label class="label" for="id_languages">{% trans "Languages:" %}</label> <label class="label" for="id_languages">{% trans "Languages:" %}</label>
@ -171,7 +67,12 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
</div>
</section>
<section class="block">
<h2 class="title is-4">{% trans "Publication" %}</h2>
<div class="box">
<div class="field"> <div class="field">
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label> <label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
{{ form.publishers }} {{ form.publishers }}
@ -196,10 +97,12 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
</div>
</section> </section>
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Authors" %}</h2> <h2 class="title is-4">{% trans "Authors" %}</h2>
<div class="box">
{% if book.authors.exists %} {% if book.authors.exists %}
<fieldset> <fieldset>
{% for author in book.authors.all %} {% 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 %}> <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> <span class="help">{% trans "Separate multiple values with commas." %}</span>
</div> </div>
</div>
</section> </section>
</div> </div>
<div class="column is-half"> <div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Cover" %}</h2> <h2 class="title is-4">{% trans "Cover" %}</h2>
<div class="box">
<div class="columns"> <div class="columns">
{% if book.cover %}
<div class="column is-3 is-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' %} {% 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>
{% endif %}
<div class="column"> <div class="column">
<div class="block">
<div class="field"> <div class="field">
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label> <label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
{{ form.cover }} {{ form.cover }}
@ -248,9 +155,11 @@
</div> </div>
</div> </div>
</div> </div>
</section>
<div class="block"> <section class="block">
<h2 class="title is-4">{% trans "Physical Properties" %}</h2> <h2 class="title is-4">{% trans "Physical Properties" %}</h2>
<div class="box">
<div class="columns"> <div class="columns">
<div class="column is-one-third"> <div class="column is-one-third">
<div class="field"> <div class="field">
@ -282,9 +191,11 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</section>
<div class="block"> <section class="block">
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2> <h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
<div class="box">
<div class="field"> <div class="field">
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label> <label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
{{ form.isbn_13 }} {{ form.isbn_13 }}
@ -333,15 +244,6 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</section>
</div> </div>
</div> </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 %}

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> <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> </div>
{% include 'book/edition_filters.html' %} {% include 'book/editions/edition_filters.html' %}
<div class="block"> <div class="block">
{% for book in editions %} {% for book in editions %}

View file

@ -3,22 +3,20 @@
{% load i18n %} {% load i18n %}
{% load humanize %} {% 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> <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 %} {% if format and not pages %}
{% blocktrans %}{{ format }}{% endblocktrans %} {% blocktrans %}{{ format }}{% endblocktrans %}
{% elif format and pages %} {% elif format and pages %}
@ -26,8 +24,9 @@
{% elif pages %} {% elif pages %}
{% blocktrans %}{{ pages }} pages{% endblocktrans %} {% blocktrans %}{{ pages }} pages{% endblocktrans %}
{% endif %} {% endif %}
{% endwith %}
</p> </p>
{% endif %}
{% endwith %}
{% if book.languages %} {% if book.languages %}
{% for language in book.languages %} {% for language in book.languages %}
@ -41,14 +40,15 @@
</p> </p>
{% endif %} {% endif %}
<p> {% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
{% 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 %} {% if date or book.first_published_date %}
<meta <meta
itemprop="datePublished" itemprop="datePublished"
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}" content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
> >
{% endif %} {% endif %}
<p>
{% comment %} {% comment %}
@todo The publisher property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor. @todo The publisher property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor.
@ -67,6 +67,7 @@
{% elif publisher %} {% elif publisher %}
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %} {% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
{% endif %} {% endif %}
{% endwith %}
</p> </p>
{% endif %}
{% endwith %}
{% endspaceless %} {% endspaceless %}

View file

@ -12,7 +12,7 @@ draft: an existing Status object that is providing default values for input fiel
name="content" name="content"
class="textarea save-draft" class="textarea save-draft"
data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}" 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 }}" placeholder="{{ placeholder }}"
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}" aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
{% if not optional and type != "quotation" and type != "review" %}required{% 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 }}" name="{{ type }}"
action="/post/{{ type }}" action="/post/{{ type }}"
method="post" method="post"
id="tab_{{ type }}_{{ book.id }}{{ reply_parent.id }}" id="form_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
> >
{% endblock %} {% 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) #} {# fields that go between the content warnings and the content field (ie, quote) #}
{% block pre_content_additions %}{% endblock %} {% 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 %} {% block content_label %}
{% trans "Comment:" %} {% trans "Comment:" %}
{% endblock %} {% endblock %}

View file

@ -1,6 +1,10 @@
{% load i18n %} {% load i18n %}
<div class="content"> <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 %}"> <form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}">
{% csrf_token %} {% csrf_token %}
@ -20,7 +24,7 @@
<div class="column"> <div class="column">
<label class="label" for="privacy_{{ goal.id }}">{% trans "Goal privacy:" %}</label> <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>
</div> </div>

View file

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

View file

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

View file

@ -35,3 +35,7 @@ Finish "<em>{{ book_title }}</em>"
</div> </div>
</div> </div>
{% endblock %} {% 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="mention_books" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
{% endblock %} {% endblock %}
{% block form_close %}{% endblock %}

View file

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

View file

@ -12,6 +12,10 @@
{% endblock %} {% endblock %}
{% block reading-dates %} {% 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 %} {% include "snippets/progress_field.html" with progress_required=True %}
{% endblock %} {% 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" %}"> <input type="date" name="start_date" class="input" id="start_id_start_date_{{ uuid }}" value="{% now "Y-m-d" %}">
</div> </div>
{% endblock %} {% 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"> <input type="hidden" name="reading_status" value="to-read">
{% csrf_token %} {% csrf_token %}
{% endblock %} {% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=True type="want_modal" %}
{% endblock %}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
{% load utilities %}
{% if fallback_url %} {% 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 %} {% endif %}
<button <button
{% if not fallback_url %} {% 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 """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class DashboardViews(TestCase): class DashboardViews(TestCase):
@ -35,8 +35,5 @@ class DashboardViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -8,6 +7,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
class UserAdminViews(TestCase): class UserAdminViews(TestCase):
@ -36,10 +36,7 @@ class UserAdminViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_user_admin_page(self): def test_user_admin_page(self):
@ -52,10 +49,7 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id) result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -77,10 +71,7 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id) result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual( self.assertEqual(
list(self.local_user.groups.values_list("name", flat=True)), ["editor"] 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 """ """ test for app action functionality """
from io import BytesIO
import pathlib
from unittest.mock import patch from unittest.mock import patch
from PIL import Image
import responses import responses
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType 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.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils import timezone
from bookwyrm import forms, models, views 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""" """books books books"""
def setUp(self): def setUp(self):
@ -53,102 +47,6 @@ class BookViews(TestCase):
models.SiteSettings.objects.create() 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): def test_edit_book_page(self):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.EditBook.as_view() view = views.EditBook.as_view()
@ -157,7 +55,7 @@ class BookViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request, self.book.id) result = view(request, self.book.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_edit_book(self): def test_edit_book(self):
@ -188,7 +86,7 @@ class BookViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request, self.book.id) result = view(request, self.book.id)
result.render() validate_html(result.render())
# the changes haven't been saved yet # the changes haven't been saved yet
self.book.refresh_from_db() self.book.refresh_from_db()
@ -283,29 +181,12 @@ class BookViews(TestCase):
self.assertEqual(book.authors.first().name, "Sappho") self.assertEqual(book.authors.first().name, "Sappho")
self.assertEqual(book.authors.first(), book.parent_work.authors.first()) 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 @responses.activate
def test_create_book_upload_cover_url(self): def test_create_book_upload_cover_url(self):
"""create an entirely new book and work with cover url""" """create an entirely new book and work with cover url"""
self.assertFalse(self.book.cover) self.assertFalse(self.book.cover)
view = views.ConfirmEditBook.as_view()
self.local_user.groups.add(self.group) self.local_user.groups.add(self.group)
cover_url = self._setup_cover_url() cover_url = _setup_cover_url()
form = forms.EditionForm() form = forms.EditionForm()
form.data["title"] = "New Title" form.data["title"] = "New Title"
@ -322,59 +203,3 @@ class BookViews(TestCase):
self.book.refresh_from_db() self.book.refresh_from_db()
self.assertTrue(self.book.cover) 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 import models, views
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html
class BookViews(TestCase): class BookViews(TestCase):
@ -40,11 +41,11 @@ class BookViews(TestCase):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view() view = views.Editions.as_view()
request = self.factory.get("") 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertTrue("paperback" in result.context_data["formats"]) self.assertTrue("paperback" in result.context_data["formats"])
@ -57,11 +58,11 @@ class BookViews(TestCase):
) )
view = views.Editions.as_view() view = views.Editions.as_view()
request = self.factory.get("") 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 2) self.assertEqual(len(result.context_data["editions"].object_list), 2)
self.assertEqual(len(result.context_data["formats"]), 2) self.assertEqual(len(result.context_data["formats"]), 2)
@ -69,26 +70,26 @@ class BookViews(TestCase):
self.assertTrue("okay" in result.context_data["formats"]) self.assertTrue("okay" in result.context_data["formats"])
request = self.factory.get("", {"q": "fish"}) 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1) self.assertEqual(len(result.context_data["editions"].object_list), 1)
request = self.factory.get("", {"q": "okay"}) 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1) self.assertEqual(len(result.context_data["editions"].object_list), 1)
request = self.factory.get("", {"format": "okay"}) 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 is_api.return_value = False
result = view(request, self.work.id) result = view(request, self.work.id)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(len(result.context_data["editions"].object_list), 1) 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""" """there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view() view = views.Editions.as_view()
request = self.factory.get("") 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 is_api.return_value = True
result = view(request, self.work.id) result = view(request, self.work.id)
self.assertIsInstance(result, ActivitypubResponse) self.assertIsInstance(result, ActivitypubResponse)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.http.response import Http404 from django.http.response import Http404
@ -10,6 +9,7 @@ from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html
class UserViews(TestCase): class UserViews(TestCase):
@ -56,16 +56,7 @@ class UserViews(TestCase):
is_api.return_value = False is_api.return_value = False
result = view(request, "mouse") result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
request.user = self.anonymous_user request.user = self.anonymous_user
@ -73,16 +64,7 @@ class UserViews(TestCase):
is_api.return_value = False is_api.return_value = False
result = view(request, "mouse") result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.user.is_api_request") as is_api: with patch("bookwyrm.views.user.is_api_request") as is_api:
@ -111,16 +93,7 @@ class UserViews(TestCase):
is_api.return_value = False is_api.return_value = False
result = view(request, "mouse") result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.user.is_api_request") as is_api: with patch("bookwyrm.views.user.is_api_request") as is_api:
@ -151,16 +124,7 @@ class UserViews(TestCase):
is_api.return_value = False is_api.return_value = False
result = view(request, "mouse") result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
html = result.render() validate_html(result.render())
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.user.is_api_request") as is_api: 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.delete_user import DeleteUser
from .preferences.block import Block, unblock 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 # misc views
from .author import Author, EditAuthor 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 .directory import Directory
from .discover import Discover from .discover import Discover
from .editions import Editions, switch_edition
from .feed import DirectMessage, Feed, Replies, Status from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request 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! """ """ the good stuff! the books! """
from uuid import uuid4
from dateutil.parser import parse as dateparse from dateutil.parser import parse as dateparse
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector 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 import transaction
from django.db.models import Avg, Q from django.http import HttpResponseBadRequest
from django.http import HttpResponseBadRequest, Http404
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.datastructures import MultiValueDictKeyError from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.abstract_connector import get_image from bookwyrm.views.helpers import get_edition
from bookwyrm.settings import PAGE_LENGTH from .books import set_cover_from_url
from .helpers import is_api_request, get_edition, privacy_filter
# pylint: disable=no-self-use # 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(login_required, name="dispatch")
@method_decorator( @method_decorator(
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
@ -138,7 +32,7 @@ class EditBook(View):
if not book.description: if not book.description:
book.description = book.parent_work.description book.description = book.parent_work.description
data = {"book": book, "form": forms.EditionForm(instance=book)} 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): def post(self, request, book_id=None):
"""edit a book cool""" """edit a book cool"""
@ -148,7 +42,7 @@ class EditBook(View):
data = {"book": book, "form": form} data = {"book": book, "form": form}
if not form.is_valid(): 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") add_author = request.POST.get("add_author")
# we're adding an author through a free text field # we're adding an author through a free text field
@ -207,7 +101,7 @@ class EditBook(View):
except (MultiValueDictKeyError, ValueError): except (MultiValueDictKeyError, ValueError):
pass pass
data["form"].data = formcopy 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") remove_authors = request.POST.getlist("remove_authors")
for author_id in remove_authors: for author_id in remove_authors:
@ -238,7 +132,7 @@ class ConfirmEditBook(View):
data = {"book": book, "form": form} data = {"book": book, "form": form}
if not form.is_valid(): 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(): with transaction.atomic():
# save book # save book
@ -284,67 +178,3 @@ class ConfirmEditBook(View):
book.save(broadcast=False) book.save(broadcast=False)
return redirect(f"/book/{book.id}") 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 import models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH 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 # pylint: disable=no-self-use
@ -66,7 +66,7 @@ class Editions(View):
e.physical_format.lower() for e in editions if e.physical_format 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 @login_required