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): class ReadThrough(BookWyrmModel):
''' Store progress through a book in the database. ''' ''' Store progress through a book in the database. '''
user = models.ForeignKey('User', on_delete=models.PROTECT) 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( pages_read = models.IntegerField(
null=True, null=True,
blank=True) blank=True)

View file

@ -91,87 +91,26 @@
{% endif %} {% endif %}
</div> </div>
{% for readthrough in readthroughs %} {# user's relationship to the book #}
<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> <div>
<input class="toggle-control" type="checkbox" name="delete-readthrough-{{ readthrough.id }}" id="delete-readthrough-{{ readthrough.id }}"> {% for shelf in user_shelves %}
<div class="modal toggle-content hidden"> <p>
<div class="modal-background"></div> This edition is on your <a href="/user/{{ user.localname }}/shelves/{{ shelf.shelf.identifier }}">{{ shelf.shelf.name }}</a> shelf.
<div class="modal-card"> {% include 'snippets/shelf_selector.html' with current=shelf.shelf %}
<header class="modal-card-head"> </p>
<p class="modal-card-title">Delete this read-though?</p> {% endfor %}
<label class="delete" for="delete-readthrough-{{ readthrough.id }}" aria-label="close"></label>
</header> {% for shelf in other_edition_shelves %}
<footer class="modal-card-foot"> <p>
<form name="delete-readthrough-{{ readthrough.id }}" action="/delete-readthrough" method="POST"> 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.
{% csrf_token %} {% include 'snippets/switch_edition_button.html' with edition=book %}
<input type="hidden" name="id" value="{{ readthrough.id }}"> </p>
<button class="button is-danger is-light" type="submit"> {% endfor %}
Delete
</button> {% for readthrough in readthroughs %}
<label for="delete-readthrough-{{ readthrough.id }}" class="button" role="button" tabindex="0">Cancel</button> {% include 'snippets/readthrough.html' with readthrough=readthrough %}
</form> {% endfor %}
</footer>
</div>
<label class="modal-close is-large" for="delete-readthrough-{{ readthrough.id }}" aria-label="close"></label>
</div>
</div> </div>
{% endfor %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="box"> <div class="box">

View file

@ -1,16 +1,11 @@
<div class="columns"> <div class="columns is-multiline">
{% for book in books %} {% for book in books %}
{% if forloop.counter0|divisibleby:"4" %}
</div>
<div class="columns">
{% endif %}
<div class="column is-narrow"> <div class="column is-narrow">
<div class="box"> <div class="box">
<a href="/book/{{ book.id }}"> <a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book %}
</a> </a>
{% include 'snippets/rate_action.html' with user=request.user book=book %} {% include 'snippets/shelve_button.html' with book=book switch_mode=True %}
{% include 'snippets/shelve_button.html' with book=book %}
</div> </div>
</div> </div>
{% endfor %} {% 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 %} {% with book.id|uuid as uuid %}
{% active_shelf book as active_shelf %} {% active_shelf book as active_shelf %}
<div class="field is-grouped"> <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> <button class="button is-small" disabled>
<span>Read</span> <span class="icon icon-check"></span> <span>Read</span> <span class="icon icon-check"></span>
</button> </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"> <label class="button is-small" for="finish-reading-{{ uuid }}" role="button" tabindex="0">
I'm done! I'm done!
</label> </label>
{% include 'snippets/finish_reading_modal.html' %} {% include 'snippets/finish_reading_modal.html' with book=active_shelf.book %}
{% elif active_shelf.identifier == 'to-read' %} {% elif active_shelf.shelf.identifier == 'to-read' %}
<label class="button is-small" for="start-reading-{{ uuid }}" role="button" tabindex="0"> <label class="button is-small" for="start-reading-{{ uuid }}" role="button" tabindex="0">
Start reading Start reading
</label> </label>
{% include 'snippets/start_reading_modal.html' %} {% include 'snippets/start_reading_modal.html' with book=active_shelf.book %}
{% else %} {% else %}
<form name="shelve" action="/shelve/" method="post"> <form name="shelve" action="/shelve/" method="post">
{% csrf_token %} {% 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"> <input type="hidden" name="shelf" value="to-read">
<button class="button is-small" type="submit">Want to read</button> <button class="button is-small" type="submit">Want to read</button>
</form> </form>
@ -40,17 +44,17 @@
<ul class="dropdown-content"> <ul class="dropdown-content">
{% for shelf in request.user.shelf_set.all %} {% for shelf in request.user.shelf_set.all %}
<li role="menuitem"> <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"> <div class="dropdown-item pt-0 pb-0">
<label class="button is-small" for="start-reading-{{ uuid }}" role="button" tabindex="0"> <label class="button is-small" for="start-reading-{{ uuid }}" role="button" tabindex="0">
Start reading Start reading
</label> </label>
{% include 'snippets/start_reading_modal.html' %} {% include 'snippets/start_reading_modal.html' with book=active_shelf.book %}
</div> </div>
{% else %} {% else %}
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/shelve/" method="post"> <form class="dropdown-item pt-0 pb-0" name="shelve" action="/shelve/" method="post">
{% csrf_token %} {% 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 %}> <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> <span>{{ shelf.name }}</span>
{% if shelf in book.shelf_set.all %}<span class="icon icon-check"></span>{% endif %} {% if shelf in book.shelf_set.all %}<span class="icon icon-check"></span>{% endif %}
@ -62,6 +66,7 @@
</ul> </ul>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% 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 delta = now - date
if date < (now - relativedelta(weeks=1)): if date < (now - relativedelta(weeks=1)):
formatter = '%b %-d'
if date.year != now.year: if date.year != now.year:
return date.strftime('%b %-d %Y') formatter += ' %Y'
return date.strftime('%b %-d') return date.strftime(formatter)
delta = relativedelta(now, date) delta = relativedelta(now, date)
if delta.days: if delta.days:
return '%dd' % 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 ''' ''' check what shelf a user has a book on, if any '''
shelf = models.ShelfBook.objects.filter( shelf = models.ShelfBook.objects.filter(
shelf__user=context['request'].user, shelf__user=context['request'].user,
book=book book__in=book.parent_work.editions.all()
).first() ).first()
return shelf.shelf if shelf else None return shelf if shelf else {'book': book}
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)

View file

@ -237,3 +237,28 @@ class ViewActions(TestCase):
resp = actions.password_reset(request) resp = actions.password_reset(request)
self.assertEqual(resp.template_name, 'password_reset.html') self.assertEqual(resp.template_name, 'password_reset.html')
self.assertTrue(models.PasswordReset.objects.exists()) 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'^edit-profile/?$', actions.edit_profile),
re_path(r'^import-data/?', actions.import_data), re_path(r'^import-data/?$', actions.import_data),
re_path(r'^retry-import/?', actions.retry_import), re_path(r'^retry-import/?$', actions.retry_import),
re_path(r'^resolve-book/?', actions.resolve_book), re_path(r'^resolve-book/?$', actions.resolve_book),
re_path(r'^edit-book/(?P<book_id>\d+)/?', actions.edit_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'^upload-cover/(?P<book_id>\d+)/?$', actions.upload_cover),
re_path(r'^add-description/(?P<book_id>\d+)/?', actions.add_description), re_path(r'^add-description/(?P<book_id>\d+)/?$', actions.add_description),
re_path(r'^edit-readthrough/?', actions.edit_readthrough), re_path(r'^switch-edition/?$', actions.switch_edition),
re_path(r'^delete-readthrough/?', actions.delete_readthrough), 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'^rate/?$', actions.rate),
re_path(r'^review/?$', actions.review), 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.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
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
@ -17,6 +18,7 @@ from django.utils import timezone
from django.views.decorators.http import require_GET, require_POST from django.views.decorators.http import require_GET, require_POST
from bookwyrm import books_manager from bookwyrm import books_manager
from bookwyrm.broadcast import broadcast
from bookwyrm import forms, models, outgoing from bookwyrm import forms, models, outgoing
from bookwyrm import goodreads_import from bookwyrm import goodreads_import
from bookwyrm.emailing import password_reset_email from bookwyrm.emailing import password_reset_email
@ -246,6 +248,36 @@ def edit_book(request, book_id):
return redirect('/book/%s' % 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 @login_required
@require_POST @require_POST
def upload_cover(request, book_id): def upload_cover(request, book_id):

View file

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