Merge pull request #382 from mouse-reeve/switch-edition

Adds shelf info to book page
This commit is contained in:
Mouse Reeve 2020-12-16 14:58:36 -08:00 committed by GitHub
commit ef92e562fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 252 additions and 112 deletions

View file

@ -0,0 +1,19 @@
# Generated by Django 3.0.7 on 2020-12-12 00:59
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0016_auto_20201211_2026'),
]
operations = [
migrations.AlterField(
model_name='readthrough',
name='book',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
),
]

View file

@ -0,0 +1,14 @@
# Generated by Django 3.0.7 on 2020-12-16 01:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0017_auto_20201212_0059'),
('bookwyrm', '0022_auto_20201212_1744'),
]
operations = [
]

View file

@ -0,0 +1,14 @@
# Generated by Django 3.0.7 on 2020-12-16 17:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0023_auto_20201214_0511'),
('bookwyrm', '0023_merge_20201216_0112'),
]
operations = [
]

View file

@ -240,7 +240,7 @@ class Boost(Status):
class ReadThrough(BookWyrmModel):
''' Store progress through a book in the database. '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
book = models.ForeignKey('Book', on_delete=models.PROTECT)
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
pages_read = models.IntegerField(
null=True,
blank=True)

View file

@ -91,87 +91,26 @@
{% endif %}
</div>
{% for readthrough in readthroughs %}
<div class="content block">
<input class="toggle-control" type="radio" name="show-edit-readthrough" id="show-readthrough-{{ readthrough.id }}" checked>
<div class="toggle-content hidden">
<dl>
{% if readthrough.start_date %}
<dt>Started reading:</dt>
<dd>{{ readthrough.start_date | naturalday }}</dd>
{% endif %}
{% if readthrough.finish_date %}
<dt>Finished reading:</dt>
<dd>{{ readthrough.finish_date | naturalday }}</dd>
{% endif %}
</dl>
<div class="field is-grouped">
<label class="button is-small" for="edit-readthrough-{{ readthrough.id }}" role="button" tabindex="0">
<span class="icon icon-pencil">
<span class="is-sr-only">Edit read-through dates</span>
</span>
</label>
<label class="button is-small" for="delete-readthrough-{{ readthrough.id }}" role="button" tabindex="0">
<span class="icon icon-x">
<span class="is-sr-only">Delete this read-through</span>
</span>
</label>
</div>
</div>
</div>
<div class="block">
<input class="toggle-control" type="radio" name="show-edit-readthrough" id="edit-readthrough-{{ readthrough.id }}">
<div class="toggle-content hidden">
<div class="box">
<form name="edit-readthrough" action="/edit-readthrough" method="post">
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<div class="field">
<label class="label">
Started reading
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</label>
</div>
<div class="field">
<label class="label">
Finished reading
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{{ readthrough.finish_date | date:"Y-m-d" }}">
</label>
</div>
<div class="field is-grouped">
<button class="button is-primary" type="submit">Save</button>
<label class="button" for="show-readthrough-{{ readthrough.id }}" role="button" tabindex="0">Cancel</label>
</div>
</form>
</div>
</div>
</div>
{# user's relationship to the book #}
<div>
<input class="toggle-control" type="checkbox" name="delete-readthrough-{{ readthrough.id }}" id="delete-readthrough-{{ readthrough.id }}">
<div class="modal toggle-content hidden">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete this read-though?</p>
<label class="delete" for="delete-readthrough-{{ readthrough.id }}" aria-label="close"></label>
</header>
<footer class="modal-card-foot">
<form name="delete-readthrough-{{ readthrough.id }}" action="/delete-readthrough" method="POST">
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<button class="button is-danger is-light" type="submit">
Delete
</button>
<label for="delete-readthrough-{{ readthrough.id }}" class="button" role="button" tabindex="0">Cancel</button>
</form>
</footer>
</div>
<label class="modal-close is-large" for="delete-readthrough-{{ readthrough.id }}" aria-label="close"></label>
</div>
{% for shelf in user_shelves %}
<p>
This edition is on your <a href="/user/{{ user.localname }}/shelves/{{ shelf.shelf.identifier }}">{{ shelf.shelf.name }}</a> shelf.
{% include 'snippets/shelf_selector.html' with current=shelf.shelf %}
</p>
{% endfor %}
{% for shelf in other_edition_shelves %}
<p>
A <a href="/book/{{ shelf.book.id }}">different edition</a> of this book is on your <a href="/user/{{ user.localname }}/shelves/{{ shelf.shelf.identifier }}">{{ shelf.shelf.name }}</a> shelf.
{% include 'snippets/switch_edition_button.html' with edition=book %}
</p>
{% endfor %}
{% for readthrough in readthroughs %}
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
{% endfor %}
</div>
{% endfor %}
{% if request.user.is_authenticated %}
<div class="box">

View file

@ -1,16 +1,11 @@
<div class="columns">
<div class="columns is-multiline">
{% for book in books %}
{% if forloop.counter0|divisibleby:"4" %}
</div>
<div class="columns">
{% endif %}
<div class="column is-narrow">
<div class="box">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
{% include 'snippets/shelve_button.html' with book=book switch_mode=True %}
</div>
</div>
{% endfor %}

View file

@ -0,0 +1,80 @@
{% load humanize %}
<div class="content block">
<input class="toggle-control" type="radio" name="show-edit-readthrough" id="show-readthrough-{{ readthrough.id }}" checked>
<div class="toggle-content hidden">
<dl>
{% if readthrough.start_date %}
<dt>Started reading:</dt>
<dd>{{ readthrough.start_date | naturalday }}</dd>
{% endif %}
{% if readthrough.finish_date %}
<dt>Finished reading:</dt>
<dd>{{ readthrough.finish_date | naturalday }}</dd>
{% endif %}
</dl>
<div class="field is-grouped">
<label class="button is-small" for="edit-readthrough-{{ readthrough.id }}" role="button" tabindex="0">
<span class="icon icon-pencil">
<span class="is-sr-only">Edit read-through dates</span>
</span>
</label>
<label class="button is-small" for="delete-readthrough-{{ readthrough.id }}" role="button" tabindex="0">
<span class="icon icon-x">
<span class="is-sr-only">Delete this read-through</span>
</span>
</label>
</div>
</div>
</div>
<div class="block">
<input class="toggle-control" type="radio" name="show-edit-readthrough" id="edit-readthrough-{{ readthrough.id }}">
<div class="toggle-content hidden">
<div class="box">
<form name="edit-readthrough" action="/edit-readthrough" method="post">
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<div class="field">
<label class="label">
Started reading
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</label>
</div>
<div class="field">
<label class="label">
Finished reading
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{{ readthrough.finish_date | date:"Y-m-d" }}">
</label>
</div>
<div class="field is-grouped">
<button class="button is-primary" type="submit">Save</button>
<label class="button" for="show-readthrough-{{ readthrough.id }}" role="button" tabindex="0">Cancel</label>
</div>
</form>
</div>
</div>
</div>
<div>
<input class="toggle-control" type="checkbox" name="delete-readthrough-{{ readthrough.id }}" id="delete-readthrough-{{ readthrough.id }}">
<div class="modal toggle-content hidden">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete this read-though?</p>
<label class="delete" for="delete-readthrough-{{ readthrough.id }}" aria-label="close"></label>
</header>
<footer class="modal-card-foot">
<form name="delete-readthrough-{{ readthrough.id }}" action="/delete-readthrough" method="POST">
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<button class="button is-danger is-light" type="submit">
Delete
</button>
<label for="delete-readthrough-{{ readthrough.id }}" class="button" role="button" tabindex="0">Cancel</button>
</form>
</footer>
</div>
<label class="modal-close is-large" for="delete-readthrough-{{ readthrough.id }}" aria-label="close"></label>
</div>
</div>

View file

@ -4,24 +4,28 @@
{% with book.id|uuid as uuid %}
{% active_shelf book as active_shelf %}
<div class="field is-grouped">
{% if active_shelf.identifier == 'read' %}
{% if switch_mode and active_shelf.book != book %}
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}
{% else %}
{% if active_shelf.shelf.identifier == 'read' %}
<button class="button is-small" disabled>
<span>Read</span> <span class="icon icon-check"></span>
</button>
{% elif active_shelf.identifier == 'reading' %}
{% elif active_shelf.shelf.identifier == 'reading' %}
<label class="button is-small" for="finish-reading-{{ uuid }}" role="button" tabindex="0">
I'm done!
</label>
{% include 'snippets/finish_reading_modal.html' %}
{% elif active_shelf.identifier == 'to-read' %}
{% include 'snippets/finish_reading_modal.html' with book=active_shelf.book %}
{% elif active_shelf.shelf.identifier == 'to-read' %}
<label class="button is-small" for="start-reading-{{ uuid }}" role="button" tabindex="0">
Start reading
</label>
{% include 'snippets/start_reading_modal.html' %}
{% include 'snippets/start_reading_modal.html' with book=active_shelf.book %}
{% else %}
<form name="shelve" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="to-read">
<button class="button is-small" type="submit">Want to read</button>
</form>
@ -40,17 +44,17 @@
<ul class="dropdown-content">
{% for shelf in request.user.shelf_set.all %}
<li role="menuitem">
{% if shelf.identifier == 'to-read' %}
{% if active_shelf.shelf.identifier == 'to-read' and shelf.identifier == 'reading' %}
<div class="dropdown-item pt-0 pb-0">
<label class="button is-small" for="start-reading-{{ uuid }}" role="button" tabindex="0">
Start reading
</label>
{% include 'snippets/start_reading_modal.html' %}
{% include 'snippets/start_reading_modal.html' with book=active_shelf.book %}
</div>
{% else %}
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<button class="button is-small" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
<span>{{ shelf.name }}</span>
{% if shelf in book.shelf_set.all %}<span class="icon icon-check"></span>{% endif %}
@ -62,6 +66,7 @@
</ul>
</div>
</div>
{% endif %}
</div>
{% endwith %}
{% endif %}

View file

@ -0,0 +1,5 @@
<form name="switch-edition" action="/switch-edition" method="POST">
{% csrf_token %}
<input type="hidden" name="edition" value="{{ edition.id }}">
<button class="button {{ size }}">Switch to this edition</button>
</form>

View file

@ -132,9 +132,10 @@ def time_since(date):
delta = now - date
if date < (now - relativedelta(weeks=1)):
formatter = '%b %-d'
if date.year != now.year:
return date.strftime('%b %-d %Y')
return date.strftime('%b %-d')
formatter += ' %Y'
return date.strftime(formatter)
delta = relativedelta(now, date)
if delta.days:
return '%dd' % delta.days
@ -150,9 +151,9 @@ def active_shelf(context, book):
''' check what shelf a user has a book on, if any '''
shelf = models.ShelfBook.objects.filter(
shelf__user=context['request'].user,
book=book
book__in=book.parent_work.editions.all()
).first()
return shelf.shelf if shelf else None
return shelf if shelf else {'book': book}
@register.simple_tag(takes_context=False)

View file

@ -237,3 +237,28 @@ class ViewActions(TestCase):
resp = actions.password_reset(request)
self.assertEqual(resp.template_name, 'password_reset.html')
self.assertTrue(models.PasswordReset.objects.exists())
def test_switch_edition(self):
''' updates user's relationships to a book '''
work = models.Work.objects.create(title='test work')
edition1 = models.Edition.objects.create(
title='first ed', parent_work=work)
edition2 = models.Edition.objects.create(
title='second ed', parent_work=work)
shelf = models.Shelf.objects.create(
name='Test Shelf', user=self.local_user)
shelf.books.add(edition1)
models.ReadThrough.objects.create(
user=self.local_user, book=edition1)
self.assertEqual(models.ShelfBook.objects.get().book, edition1)
self.assertEqual(models.ReadThrough.objects.get().book, edition1)
request = self.factory.post('', {
'edition': edition2.id
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
actions.switch_edition(request)
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
self.assertEqual(models.ReadThrough.objects.get().book, edition2)

View file

@ -98,15 +98,16 @@ urlpatterns = [
re_path(r'^edit-profile/?$', actions.edit_profile),
re_path(r'^import-data/?', actions.import_data),
re_path(r'^retry-import/?', actions.retry_import),
re_path(r'^resolve-book/?', actions.resolve_book),
re_path(r'^edit-book/(?P<book_id>\d+)/?', actions.edit_book),
re_path(r'^upload-cover/(?P<book_id>\d+)/?', actions.upload_cover),
re_path(r'^add-description/(?P<book_id>\d+)/?', actions.add_description),
re_path(r'^import-data/?$', actions.import_data),
re_path(r'^retry-import/?$', actions.retry_import),
re_path(r'^resolve-book/?$', actions.resolve_book),
re_path(r'^edit-book/(?P<book_id>\d+)/?$', actions.edit_book),
re_path(r'^upload-cover/(?P<book_id>\d+)/?$', actions.upload_cover),
re_path(r'^add-description/(?P<book_id>\d+)/?$', actions.add_description),
re_path(r'^edit-readthrough/?', actions.edit_readthrough),
re_path(r'^delete-readthrough/?', actions.delete_readthrough),
re_path(r'^switch-edition/?$', actions.switch_edition),
re_path(r'^edit-readthrough/?$', actions.edit_readthrough),
re_path(r'^delete-readthrough/?$', actions.delete_readthrough),
re_path(r'^rate/?$', actions.rate),
re_path(r'^review/?$', actions.review),

View file

@ -10,6 +10,7 @@ from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import PermissionDenied
from django.core.files.base import ContentFile
from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
@ -17,6 +18,7 @@ from django.utils import timezone
from django.views.decorators.http import require_GET, require_POST
from bookwyrm import books_manager
from bookwyrm.broadcast import broadcast
from bookwyrm import forms, models, outgoing
from bookwyrm import goodreads_import
from bookwyrm.emailing import password_reset_email
@ -246,6 +248,36 @@ def edit_book(request, book_id):
return redirect('/book/%s' % book.id)
@login_required
@require_POST
@transaction.atomic
def switch_edition(request):
''' switch your copy of a book to a different edition '''
edition_id = request.POST.get('edition')
new_edition = get_object_or_404(models.Edition, id=edition_id)
shelfbooks = models.ShelfBook.objects.filter(
book__parent_work=new_edition.parent_work,
shelf__user=request.user
)
for shelfbook in shelfbooks.all():
broadcast(request.user, shelfbook.to_remove_activity(request.user))
shelfbook.book = new_edition
shelfbook.save()
broadcast(request.user, shelfbook.to_add_activity(request.user))
readthroughs = models.ReadThrough.objects.filter(
book__parent_work=new_edition.parent_work,
user=request.user
)
for readthrough in readthroughs.all():
readthrough.book = new_edition
readthrough.save()
return redirect('/book/%d' % new_edition.id)
@login_required
@require_POST
def upload_cover(request, book_id):

View file

@ -594,8 +594,7 @@ def book_page(request, book_id):
prev_page = '/book/%s/?page=%d' % \
(book_id, reviews_page.previous_page_number())
user_tags = []
readthroughs = []
user_tags = readthroughs = user_shelves = other_edition_shelves = []
if request.user.is_authenticated:
user_tags = models.UserTag.objects.filter(
book=book, user=request.user
@ -606,6 +605,16 @@ def book_page(request, book_id):
book=book,
).order_by('start_date')
user_shelves = models.ShelfBook.objects.filter(
added_by=request.user, book=book
)
other_edition_shelves = models.ShelfBook.objects.filter(
~Q(book=book),
added_by=request.user,
book__parent_work=book.parent_work,
)
rating = reviews.aggregate(Avg('rating'))
tags = models.UserTag.objects.filter(
book=book,
@ -619,6 +628,8 @@ def book_page(request, book_id):
'rating': rating['rating__avg'],
'tags': tags,
'user_tags': user_tags,
'user_shelves': user_shelves,
'other_edition_shelves': other_edition_shelves,
'readthroughs': readthroughs,
'path': '/book/%s' % book_id,
'info_fields': [
@ -662,10 +673,9 @@ def editions_page(request, book_id):
encoder=ActivityEncoder
)
editions = models.Edition.objects.filter(parent_work=work).all()
data = {
'title': 'Editions of %s' % work.title,
'editions': editions,
'editions': work.editions.all(),
'work': work,
}
return TemplateResponse(request, 'editions.html', data)