Merge pull request #290 from mouse-reeve/read-progress

Read progress
This commit is contained in:
Mouse Reeve 2020-11-06 09:08:49 -08:00 committed by GitHub
commit d31071ddb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 335 additions and 127 deletions

View file

@ -1,5 +1,4 @@
''' handles all the activity coming out of the server ''' ''' handles all the activity coming out of the server '''
from datetime import datetime
import re import re
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
@ -121,6 +120,19 @@ def handle_shelve(user, book, shelf):
broadcast(user, shelve.to_add_activity(user)) broadcast(user, shelve.to_add_activity(user))
def handle_unshelve(user, book, shelf):
''' a local user is getting a book put on their shelf '''
# update the database
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
activity = row.to_remove_activity(user)
row.delete()
broadcast(user, activity)
def handle_reading_status(user, shelf, book, privacy):
''' post about a user reading a book '''
# tell the world about this cool thing that happened # tell the world about this cool thing that happened
try: try:
message = { message = {
@ -132,41 +144,17 @@ def handle_shelve(user, book, shelf):
# it's a non-standard shelf, don't worry about it # it's a non-standard shelf, don't worry about it
return return
status = create_generated_note(user, message, mention_books=[book]) status = create_generated_note(
user,
message,
mention_books=[book],
privacy=privacy
)
status.save() status.save()
if shelf.identifier == 'reading':
read = models.ReadThrough(
user=user,
book=book,
start_date=datetime.now())
read.save()
elif shelf.identifier == 'read':
read = models.ReadThrough.objects.filter(
user=user,
book=book,
finish_date=None).order_by('-created_date').first()
if not read:
read = models.ReadThrough(
user=user,
book=book,
start_date=datetime.now())
read.finish_date = datetime.now()
read.save()
broadcast(user, status.to_create_activity(user)) broadcast(user, status.to_create_activity(user))
def handle_unshelve(user, book, shelf):
''' a local user is getting a book put on their shelf '''
# update the database
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
activity = row.to_remove_activity(user)
row.delete()
broadcast(user, activity)
def handle_imported_book(user, item, include_reviews, privacy): def handle_imported_book(user, item, include_reviews, privacy):
''' process a goodreads csv and then post about it ''' ''' process a goodreads csv and then post about it '''
if isinstance(item.book, models.Work): if isinstance(item.book, models.Work):

View file

@ -15,6 +15,10 @@ input.toggle-control:checked ~ .toggle-content {
display: block; display: block;
} }
input.toggle-control:checked ~ .modal.toggle-content {
display: flex;
}
/* --- STARS --- */ /* --- STARS --- */
.rate-stars button.icon { .rate-stars button.icon {
background: none; background: none;

View file

@ -14,7 +14,7 @@ def delete_status(status):
status.save() status.save()
def create_generated_note(user, content, mention_books=None): def create_generated_note(user, content, mention_books=None, privacy='public'):
''' a note created by the app about user activity ''' ''' a note created by the app about user activity '''
# sanitize input html # sanitize input html
parser = InputHtmlParser() parser = InputHtmlParser()
@ -24,6 +24,7 @@ def create_generated_note(user, content, mention_books=None):
status = models.GeneratedNote.objects.create( status = models.GeneratedNote.objects.create(
user=user, user=user,
content=content, content=content,
privacy=privacy
) )
if mention_books: if mention_books:

View file

@ -44,14 +44,7 @@
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_review"></textarea> <textarea name="content" class="textarea" id="id_content_{{ book.id }}_review"></textarea>
</div> </div>
<div class="control is-grouped"> <div class="control is-grouped">
<div class="select"> {% include 'snippets/privacy_select.html' %}
<select name="privacy">
<option value="public" selected>Public</option>
<option value="unlisted">Unlisted</option>
<option value="followers">Followers only</option>
<option value="direct">Private</option>
</select>
</div>
<button class="button is-primary" type="submit">post review</button> <button class="button is-primary" type="submit">post review</button>
</div> </div>
</form> </form>

View file

@ -0,0 +1,9 @@
<div class="select">
<select name="privacy">
<option value="public" selected>Public</option>
<option value="unlisted">Unlisted</option>
<option value="followers">Followers only</option>
<option value="direct">Private</option>
</select>
</div>

View file

@ -13,14 +13,7 @@
<div class="column is-narrow"> <div class="column is-narrow">
<div class="field"> <div class="field">
<div class="select"> {% include 'snippets/privacy_select.html' %}
<select name="privacy">
<option value="public" selected>Public</option>
<option value="unlisted">Unlisted</option>
<option value="followers">Followers only</option>
<option value="direct">Private</option>
</select>
</div>
</div> </div>
<div class="field"> <div class="field">
<button class="button is-primary" type="submit"> <button class="button is-primary" type="submit">

View file

@ -1,34 +1,145 @@
{% load fr_display %} {% load fr_display %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{% with book.id|uuid as uuid %}
{% active_shelf book as active_shelf %}
<div class="field is-grouped"> <div class="field is-grouped">
<form name="shelve" action="/shelve/" method="post"> {% if active_shelf.identifier == 'read' %}
<button class="button is-small" disabled>Read ✓</button>
{% elif active_shelf.identifier == 'reading' %}
<label class="button is-small" for="finish-reading-{{ uuid }}">
I'm done!
</label>
{% elif active_shelf.identifier == 'to-read' %}
<label class="button is-small" for="start-reading-{{ uuid }}">
Start reading
</label>
{% else %}
<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="{{ book.id }}">
<input type="hidden" name="shelf" value="{% shelve_button_identifier book %}"> <input type="hidden" name="shelf" value="to-read">
<button class="button is-small" type="submit" style="">{% shelve_button_text book %}</button> <button class="button is-small" type="submit">Want to read</button>
</form> </form>
<div class="dropdown is-hoverable"> {% endif %}
{% if not hide_pulldown %}
<div class="button dropdown-trigger is-small" > <div class="dropdown is-hoverable">
<div class="button dropdown-trigger is-small">
<span class="icon icon-arrow-down"><span class="is-sr-only">More shelves</span></span> <span class="icon icon-arrow-down"><span class="is-sr-only">More shelves</span></span>
</div> </div>
<div class="dropdown-menu"> <div class="dropdown-menu">
<ul class="dropdown-content"> <ul class="dropdown-content">
<form class="dropdown-item" name="shelve" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
{% for shelf in request.user.shelf_set.all %} {% for shelf in request.user.shelf_set.all %}
<li> <li>
<button class="is-small" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>{{ shelf.name }} {% if shelf in book.shelf_set.all %} ✓ {% endif %}</button> {% if shelf.identifier == 'reading' and active_shelf.identifier != 'reading' %}
<div class="dropdown-item pt-0 pb-0">
<label class="button is-small" for="start-reading-{{ uuid }}">
{{ shelf.name }}
</label>
</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 }}">
<button class="button is-small" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>{{ shelf.name }} {% if shelf in book.shelf_set.all %} ✓ {% endif %}</button>
</form>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
</form>
</ul> </ul>
</div> </div>
{% endif %} </div>
</div> </div>
<div>
<input class="toggle-control" type="checkbox" name="start-reading-{{ uuid }}" id="start-reading-{{ uuid }}">
<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">Started "{{ book.title }}"</p>
<label class="delete" for="start-reading-{{ uuid }}" aria-label="close"></label>
</header>
<form name="start-reading" action="/start-reading" method="post">
<section class="modal-card-body">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<div class="field">
<label class="label" for="start_date">
Started reading
<input type="date" name="start_date" class="input" id="id_start_date-{{ uuid }}" value="{% now "Y-m-d" %}">
</label>
</div>
</section>
<footer class="modal-card-foot">
<div class="columns">
<div class="column field">
<label for="post-status">
<input type="checkbox" name="post-status" class="checkbox" checked>
Post to feed
</label>
{% include 'snippets/privacy_select.html' %}
</div>
<div class="column">
<button type="submit" class="button is-success">Save</button>
<label for="start-reading-{{ uuid }}" class="button">Cancel</button>
</div>
</div>
</footer>
</form>
</div>
<label class="modal-close is-large" for="finish-reading-{{ uuid }}" aria-label="close"></label>
</div>
</div> </div>
<div>
<input class="toggle-control" type="checkbox" name="finish-reading-{{ uuid }}" id="finish-reading-{{ uuid }}">
<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">Finished "{{ book.title }}"</p>
<label class="delete" for="finish-reading-{{ uuid }}" aria-label="close"></label>
</header>
{% active_read_through book user as readthrough %}
<form name="finish-reading" action="/finish-reading" method="post">
<section class="modal-card-body">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="id" value="{{ readthrough.id }}">
<div class="field">
<label class="label" for="start_date">
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" for="finish_date">
Finished reading
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{% now "Y-m-d" %}">
</label>
</div>
</section>
<footer class="modal-card-foot">
<div class="columns">
<div class="column field">
<label for="post-status">
<input type="checkbox" name="post-status" class="checkbox" checked>
Post to feed
</label>
{% include 'snippets/privacy_select.html' %}
</div>
<div class="column">
<button type="submit" class="button is-success">Save</button>
<label for="finish-reading-{{ uuid }}" class="button">Cancel</button>
</div>
</div>
</footer>
</form>
</div>
<label class="modal-close is-large" for="finish-reading-{{ uuid }}" aria-label="close"></label>
</div>
</div>
{% endwith %}
{% endif %} {% endif %}

View file

@ -158,22 +158,14 @@ def time_since(date):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def shelve_button_identifier(context, book): 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 '''
#TODO: books can be on multiple shelves, handle that better #TODO: books can be on multiple shelves, handle that better
shelf = models.ShelfBook.objects.filter( shelf = models.ShelfBook.objects.filter(
shelf__user=context['request'].user, shelf__user=context['request'].user,
book=book book=book
).first() ).first()
if not shelf: return shelf.shelf if shelf else None
return 'to-read'
identifier = shelf.shelf.identifier
if identifier == 'to-read':
return 'reading'
if identifier == 'reading':
return 'read'
return 'to-read'
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
@ -192,7 +184,7 @@ def shelve_button_text(context, book):
return 'Start reading' return 'Start reading'
if identifier == 'reading': if identifier == 'reading':
return 'I\'m done!' return 'I\'m done!'
return 'Want to read' return 'Read'
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
@ -200,4 +192,15 @@ def latest_read_through(book, user):
''' the most recent read activity ''' ''' the most recent read activity '''
return models.ReadThrough.objects.filter( return models.ReadThrough.objects.filter(
user=user, user=user,
book=book).order_by('-created_date').first() book=book
).order_by('-start_date').first()
@register.simple_tag(takes_context=False)
def active_read_through(book, user):
''' the most recent read activity '''
return models.ReadThrough.objects.filter(
user=user,
book=book,
finish_date__isnull=True
).order_by('-start_date').first()

View file

@ -121,6 +121,8 @@ urlpatterns = [
re_path(r'^shelve/?$', actions.shelve), re_path(r'^shelve/?$', actions.shelve),
re_path(r'^unshelve/?$', actions.unshelve), re_path(r'^unshelve/?$', actions.unshelve),
re_path(r'^start-reading/?$', actions.start_reading),
re_path(r'^finish-reading/?$', actions.finish_reading),
re_path(r'^follow/?$', actions.follow), re_path(r'^follow/?$', actions.follow),
re_path(r'^unfollow/?$', actions.unfollow), re_path(r'^unfollow/?$', actions.unfollow),

View file

@ -272,51 +272,6 @@ def upload_cover(request, book_id):
return redirect('/book/%s' % book.id) return redirect('/book/%s' % book.id)
@login_required
def edit_readthrough(request):
''' can't use the form because the dates are too finnicky '''
try:
readthrough = models.ReadThrough.objects.get(id=request.POST.get('id'))
except models.ReadThrough.DoesNotExist:
return HttpResponseNotFound()
# don't let people edit other people's data
if request.user != readthrough.user:
return HttpResponseBadRequest()
# convert dates into a legible format
start_date = request.POST.get('start_date')
try:
start_date = dateutil.parser.parse(start_date)
except ParserError:
start_date = None
readthrough.start_date = start_date
finish_date = request.POST.get('finish_date')
try:
finish_date = dateutil.parser.parse(finish_date)
except ParserError:
finish_date = None
readthrough.finish_date = finish_date
readthrough.save()
return redirect(request.headers.get('Referer', '/'))
@login_required
def delete_readthrough(request):
''' remove a readthrough '''
try:
readthrough = models.ReadThrough.objects.get(id=request.POST.get('id'))
except models.ReadThrough.DoesNotExist:
return HttpResponseNotFound()
# don't let people edit other people's data
if request.user != readthrough.user:
return HttpResponseBadRequest()
readthrough.delete()
return redirect(request.headers.get('Referer', '/'))
@login_required @login_required
def shelve(request): def shelve(request):
''' put a on a user's shelf ''' ''' put a on a user's shelf '''
@ -338,6 +293,16 @@ def shelve(request):
# this just means it isn't currently on the user's shelves # this just means it isn't currently on the user's shelves
pass pass
outgoing.handle_shelve(request.user, book, desired_shelf) outgoing.handle_shelve(request.user, book, desired_shelf)
# post about "want to read" shelves
if desired_shelf.identifier == 'to-read':
outgoing.handle_reading_status(
request.user,
desired_shelf,
book,
privacy='public'
)
return redirect('/') return redirect('/')
@ -351,6 +316,107 @@ def unshelve(request):
return redirect(request.headers.get('Referer', '/')) return redirect(request.headers.get('Referer', '/'))
@login_required
def start_reading(request):
''' begin reading a book '''
book = books_manager.get_edition(request.POST['book'])
shelf = models.Shelf.objects.filter(
identifier='reading',
user=request.user
).first()
# create a readthrough
readthrough = update_readthrough(request, book=book)
if readthrough.start_date:
readthrough.save()
# shelve the book
if request.POST.get('reshelve', True):
try:
current_shelf = models.Shelf.objects.get(
user=request.user,
edition=book
)
outgoing.handle_unshelve(request.user, book, current_shelf)
except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves
pass
outgoing.handle_shelve(request.user, book, shelf)
# post about it (if you want)
if request.POST.get('post-status'):
privacy = request.POST.get('privacy')
outgoing.handle_reading_status(request.user, shelf, book, privacy)
return redirect(request.headers.get('Referer', '/'))
@login_required
def finish_reading(request):
''' a user completed a book, yay '''
book = books_manager.get_edition(request.POST['book'])
shelf = models.Shelf.objects.filter(
identifier='read',
user=request.user
).first()
# update or create a readthrough
readthrough = update_readthrough(request, book=book)
if readthrough.start_date or readthrough.finish_date:
readthrough.save()
# shelve the book
if request.POST.get('reshelve', True):
try:
current_shelf = models.Shelf.objects.get(
user=request.user,
edition=book
)
outgoing.handle_unshelve(request.user, book, current_shelf)
except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves
pass
outgoing.handle_shelve(request.user, book, shelf)
# post about it (if you want)
if request.POST.get('post-status'):
privacy = request.POST.get('privacy')
outgoing.handle_reading_status(request.user, shelf, book, privacy)
return redirect(request.headers.get('Referer', '/'))
@login_required
def edit_readthrough(request):
''' can't use the form because the dates are too finnicky '''
readthrough = update_readthrough(request, create=False)
if not readthrough:
return HttpResponseNotFound()
# don't let people edit other people's data
if request.user != readthrough.user:
return HttpResponseBadRequest()
readthrough.save()
return redirect(request.headers.get('Referer', '/'))
@login_required
def delete_readthrough(request):
''' remove a readthrough '''
try:
readthrough = models.ReadThrough.objects.get(id=request.POST.get('id'))
except models.ReadThrough.DoesNotExist:
return HttpResponseNotFound()
# don't let people edit other people's data
if request.user != readthrough.user:
return HttpResponseBadRequest()
readthrough.delete()
return redirect(request.headers.get('Referer', '/'))
@login_required @login_required
def rate(request): def rate(request):
''' just a star rating for a book ''' ''' just a star rating for a book '''
@ -578,3 +644,37 @@ def create_invite(request):
invite.save() invite.save()
return redirect('/invite') return redirect('/invite')
def update_readthrough(request, book=None, create=True):
''' updates but does not save dates on a readthrough '''
try:
read_id = request.POST.get('id')
if not read_id:
raise models.ReadThrough.DoesNotExist
readthrough = models.ReadThrough.objects.get(id=read_id)
except models.ReadThrough.DoesNotExist:
if not create or not book:
return None
readthrough = models.ReadThrough(
user=request.user,
book=book,
)
start_date = request.POST.get('start_date')
if start_date:
try:
start_date = dateutil.parser.parse(start_date)
readthrough.start_date = start_date
except ParserError:
pass
finish_date = request.POST.get('finish_date')
if finish_date:
try:
finish_date = dateutil.parser.parse(finish_date)
readthrough.finish_date = finish_date
except ParserError:
pass
return readthrough

View file

@ -71,7 +71,7 @@ def home_tab(request, tab):
models.Edition.objects.filter( models.Edition.objects.filter(
shelves__user=request.user, shelves__user=request.user,
shelves__identifier='read' shelves__identifier='read'
)[:2], ).order_by('-updated_date')[:2],
# to-read # to-read
models.Edition.objects.filter( models.Edition.objects.filter(
shelves__user=request.user, shelves__user=request.user,
@ -242,7 +242,11 @@ def about_page(request):
def password_reset_request(request): def password_reset_request(request):
''' invite management page ''' ''' invite management page '''
return TemplateResponse(request, 'password_reset_request.html', {'title': 'Reset Password'}) return TemplateResponse(
request,
'password_reset_request.html',
{'title': 'Reset Password'}
)
def password_reset(request, code): def password_reset(request, code):