Merge branch 'main' into book-file-links

This commit is contained in:
Mouse Reeve 2022-01-12 06:39:22 -08:00 committed by GitHub
commit e6d9895854
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 384 additions and 198 deletions

View file

@ -492,3 +492,19 @@ class SortListForm(forms.Form):
("descending", _("Descending")), ("descending", _("Descending")),
), ),
) )
class ReadThroughForm(CustomForm):
def clean(self):
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
finish_date = cleaned_data.get("finish_date")
if start_date > finish_date:
self.add_error(
"finish_date", _("Reading finish date cannot be before start date.")
)
class Meta:
model = models.ReadThrough
fields = ["user", "book", "start_date", "finish_date"]

View file

@ -1,5 +1,6 @@
""" progress in a book """ """ progress in a book """
from django.core import validators from django.core import validators
from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import F, Q
@ -30,6 +31,7 @@ class ReadThrough(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
self.user.update_active_date() self.user.update_active_date()
# an active readthrough must have an unset finish date # an active readthrough must have an unset finish date
if self.finish_date: if self.finish_date:

View file

@ -1,7 +1,6 @@
""" defines relationships between users """ """ defines relationships between users """
from django.apps import apps from django.apps import apps
from django.core.cache import cache from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.db import models, transaction, IntegrityError from django.db import models, transaction, IntegrityError
from django.db.models import Q from django.db.models import Q
@ -41,15 +40,12 @@ class UserRelationship(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""clear the template cache""" """clear the template cache"""
# invalidate the template cache # invalidate the template cache
cache_keys = [ cache.delete_many(
make_template_fragment_key( [
"follow_button", [self.user_subject.id, self.user_object.id] f"relationship-{self.user_subject.id}-{self.user_object.id}",
), f"relationship-{self.user_object.id}-{self.user_subject.id}",
make_template_fragment_key( ]
"follow_button", [self.user_object.id, self.user_subject.id] )
),
]
cache.delete_many(cache_keys)
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:

View file

@ -755,6 +755,13 @@ ol.ordered-list li::before {
padding: 0 0.75em; padding: 0 0.75em;
} }
/* Notifications page
******************************************************************************/
.notification a.icon {
text-decoration: none !important;
}
/* Breadcrumbs /* Breadcrumbs
******************************************************************************/ ******************************************************************************/

View file

@ -237,29 +237,21 @@
<h2 class="title is-5">{% trans "Your reading activity" %}</h2> <h2 class="title is-5">{% trans "Your reading activity" %}</h2>
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Add read dates" as button_text %} <button class="button is-small" data-modal-open="add-readthrough">
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add_readthrough" focus="add_readthrough_focus_" %} <span class="icon icon-plus m-mobile-0" aria-hidden="true"></span>
<span class="is-sr-only-mobile">
{% trans "Add read dates" %}
</span>
</button>
</div> </div>
</header> </header>
<section class="is-hidden box" id="add_readthrough"> {% include "readthrough/readthrough_modal.html" with id="add-readthrough" %}
<form name="add-readthrough" action="/create-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=None %}
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">{% trans "Create" %}</button>
</div>
<div class="control">
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="add_readthrough" %}
</div>
</div>
</form>
</section>
{% if not readthroughs.exists %} {% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p> <p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %} {% endif %}
{% for readthrough in readthroughs %} {% for readthrough in readthroughs %}
{% include 'book/readthrough.html' with readthrough=readthrough %} {% include 'readthrough/readthrough_list.html' with readthrough=readthrough %}
{% endfor %} {% endfor %}
</section> </section>
<hr aria-hidden="true"> <hr aria-hidden="true">

View file

@ -16,10 +16,7 @@
{% with shelf_counter=forloop.counter %} {% with shelf_counter=forloop.counter %}
<li> <li>
<p> <p>
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %} {% include "snippets/translated_shelf_name.html" with shelf=shelf %}
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
{% else %}{{ shelf.name }}{% endif %}
</p> </p>
<div class="tabs is-small is-toggle"> <div class="tabs is-small is-toggle">
<ul> <ul>

View file

@ -1,13 +1,17 @@
{% extends 'landing/layout.html' %} {% extends 'landing/layout.html' %}
{% load i18n %} {% load i18n %}
{% load cache %} {% load cache %}
{% load bookwyrm_tags %}
{% block panel %} {% block panel %}
<div class="block is-hidden-tablet"> <div class="block is-hidden-tablet">
<h2 class="title has-text-centered">{% trans "Recent Books" %}</h2> <h2 class="title has-text-centered">{% trans "Recent Books" %}</h2>
</div> </div>
{% cache 60 * 60 %} {% get_current_language as LANGUAGE_CODE %}
{% cache 60 * 60 LANGUAGE_CODE %}
{% get_landing_books as books %}
<section class="tile is-ancestor"> <section class="tile is-ancestor">
<div class="tile is-vertical is-6"> <div class="tile is-vertical is-6">
<div class="tile is-parent"> <div class="tile is-parent">

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% block primary_link %}{% spaceless %} {% block primary_link %}{% spaceless %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,9 +1,9 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% related_status notification as related_status %} {% related_status notification as related_status %}
<div class="box is-shadowless has-background-white-ter {% if notification.id in unread %} is-primary{% endif %}"> <div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
<div class="columns is-mobile"> <div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">
<div class="column is-narrow is-size-3 {% if notification.id in unread%}has-text-white{% else %}has-text-grey{% endif %}"> <div class="column is-narrow is-size-3">
<a class="has-text-dark" href="{% block primary_link %}{% endblock %}"> <a class="icon" href="{% block primary_link %}{% endblock %}">
{% block icon %}{% endblock %} {% block icon %}{% endblock %}
</a> </a>
</div> </div>

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -0,0 +1,15 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load utilities %}
{% block title %}
{% blocktrans trimmed with title=book|book_title %}
Update read dates for "<em>{{ title }}</em>"
{% endblocktrans %}
{% endblock %}
{% block content %}
{% include "readthrough/readthrough_modal.html" with book=book active=True static=True %}
{% endblock %}

View file

@ -4,6 +4,7 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}"> <input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="field"> <div class="field">
<label class="label" tabindex="0" id="add_readthrough_focus_{{ readthrough.id }}" for="id_start_date_{{ readthrough.id }}"> <label class="label" tabindex="0" id="add_readthrough_focus_{{ readthrough.id }}" for="id_start_date_{{ readthrough.id }}">
{% trans "Started reading" %} {% trans "Started reading" %}

View file

@ -3,7 +3,7 @@
{% load tz %} {% load tz %}
{% load utilities %} {% load utilities %}
<div class="content"> <div class="content">
<div id="hide_edit_readthrough_{{ readthrough.id }}" class="box is-shadowless has-background-white-bis"> <div class="box is-shadowless has-background-white-bis">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
{% trans "Progress Updates:" %} {% trans "Progress Updates:" %}
@ -58,7 +58,11 @@
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
{% trans "Edit read dates" as button_text %} {% trans "Edit read dates" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class="is-small" text=button_text icon="pencil" controls_text="edit_readthrough" controls_uid=readthrough.id focus="edit_readthrough" %} <button class="button is-small" type="button" data-modal-open="edit_readthrough_{{ readthrough.id }}">
<span class="icon icon-pencil" title="{{ button_text }}">
<span class="is-sr-only">{{ button_text }}</span>
</span>
</button>
</div> </div>
<div class="control"> <div class="control">
{% trans "Delete these read dates" as button_text %} {% trans "Delete these read dates" as button_text %}
@ -74,16 +78,7 @@
</div> </div>
</div> </div>
<div class="box is-hidden" id="edit_readthrough_{{ readthrough.id }}" tabindex="0"> {% join "edit_readthrough" readthrough.id as edit_modal_id %}
<h3 class="title is-5">{% trans "Edit read dates" %}</h3> {% include "readthrough/readthrough_modal.html" with readthrough=readthrough id=edit_modal_id %}
<form name="edit-readthrough" action="/edit-readthrough" method="post"> {% join "delete_readthrough" readthrough.id as delete_modal_id %}
{% include 'snippets/readthrough_form.html' with readthrough=readthrough %} {% include 'readthrough/delete_readthrough_modal.html' with id=delete_modal_id %}
<div class="field is-grouped">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="edit_readthrough" controls_uid=readthrough.id %}
</div>
</form>
</div>
{% join "delete_readthrough" readthrough.id as modal_id %}
{% include 'book/delete_readthrough_modal.html' with id=modal_id %}

View file

@ -0,0 +1,80 @@
{% extends "components/modal.html" %}
{% load i18n %}
{% load utilities %}
{% block modal-title %}
{% if readthrough %}
{% blocktrans trimmed with title=book|book_title %}
Update read dates for "<em>{{ title }}</em>"
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with title=book|book_title %}
Add read dates for "<em>{{ title }}</em>"
{% endblocktrans %}
{% endif %}
{% endblock %}
{% block modal-form-open %}
<form name="add-readthrough-{{ readthrough.id }}" action="/create-readthrough" method="post">
{% endblock %}
{% block modal-body %}
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="field">
<label class="label" tabindex="0" id="add_readthrough_focus_{{ readthrough.id }}" for="id_start_date_{{ readthrough.id }}">
{% trans "Started reading" %}
</label>
{% firstof form.start_date.value readthrough.start_date|date:"Y-m-d" as value %}
<input
type="date"
name="start_date"
class="input"
id="id_start_date_{{ readthrough.id }}"
value="{{ value }}"
aria-describedby="desc_start_date"
>
{% include 'snippets/form_errors.html' with errors_list=form.start_date.errors id="desc_start_date" %}
</div>
{# Only show progress for editing existing readthroughs #}
{% if readthrough.id and not readthrough.finish_date %}
{% join "id_progress" readthrough.id as field_id %}
<label class="label" for="{{ field_id }}">
{% trans "Progress" %}
</label>
{% include "snippets/progress_field.html" with id=field_id %}
{% endif %}
<div class="field">
<label class="label" for="id_finish_date_{{ readthrough.id }}">
{% trans "Finished reading" %}
</label>
{% firstof form.finish_date.value readthrough.finish_date|date:"Y-m-d" as value %}
<input
type="date"
name="finish_date"
class="input"
id="id_finish_date_{{ readthrough.id }}"
value="{{ value }}"
aria-describedby="desc_finish_date"
>
{% include 'snippets/form_errors.html' with errors_list=form.finish_date.errors id="desc_finish_date" %}
</div>
{% endblock %}
{% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% if not static %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endif %}
{% endblock %}
{% block modal-form-close %}
</form>
{% endblock %}

View file

@ -75,9 +75,11 @@
<form class="mt-1" action="/resolve-book" method="post"> <form class="mt-1" action="/resolve-book" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="remote_id" value="{{ result.key }}"> <input type="hidden" name="remote_id" value="{{ result.key }}">
<button type="submit" class="button is-small is-link"> <div class="control">
{% trans "Import book" %} <button type="submit" class="button is-small is-link">
</button> {% trans "Import book" %}
</button>
</div>
</form> </form>
</div> </div>
</div> </div>

View file

@ -19,6 +19,17 @@
</h1> </h1>
</header> </header>
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'user-feed' user|username %}">{% trans "User profile" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% include "snippets/translated_shelf_name.html" with shelf=shelf %}
</a>
</li>
</ul>
</nav>
<nav class="block columns is-mobile scroll-x"> <nav class="block columns is-mobile scroll-x">
<div class="column pr-0"> <div class="column pr-0">
<div class="tabs"> <div class="tabs">
@ -34,15 +45,7 @@
href="{{ shelf_tab.local_path }}" href="{{ shelf_tab.local_path }}"
{% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %} {% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %}
> >
{% if shelf_tab.identifier == 'to-read' %} {% include 'user/books_header.html' with shelf=shelf_tab %}
{% trans "To Read" %}
{% elif shelf_tab.identifier == 'reading' %}
{% trans "Currently Reading" %}
{% elif shelf_tab.identifier == 'read' %}
{% trans "Read" %}
{% else %}
{{ shelf_tab.name }}
{% endif %}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -3,16 +3,15 @@
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2"> <summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2">
<span class="mb-0 title {% if size == 'small' %}is-6{% else %}is-5{% endif %} is-flex-shrink-0"> <span class="mb-0 title {% if size == 'small' %}is-6{% else %}is-5{% endif %} is-flex-shrink-0">
{% trans "Filters" %} {% trans "Filters" %}
</span> </span>
{% if filters_applied %} {% if filters_applied %}
<span class="tag is-success is-light ml-2 mb-0 is-{{ size|default:'normal' }}"> <span class="tag is-success is-light ml-2 mb-0 is-{{ size|default:'normal' }}">
{{ _("Filters are applied") }} {% trans "Filters are applied" %}
</span> </span>
{% endif %} {% endif %}
{% if request.GET %} {% if method != "post" and request.GET %}
<span class="mb-0 tags has-addons"> <span class="mb-0 tags has-addons">
<span class="mb-0 tag is-success is-light is-{{ size|default:'normal' }}"> <span class="mb-0 tag is-success is-light is-{{ size|default:'normal' }}">
{% trans "Filters are applied" %} {% trans "Filters are applied" %}

View file

@ -1,13 +1,18 @@
{% load i18n %} {% load i18n %}
{% load interaction %}
{% if request.user == user or not request.user.is_authenticated %} {% if request.user == user or not request.user.is_authenticated %}
{% elif user in request.user.blocks.all %} {# nothing to see here -- either it's yourself or your logged out #}
{% else %}
{% get_relationship user as relationship %}
{% if relationship.is_blocked %}
{% include 'snippets/block_button.html' with blocks=True %} {% include 'snippets/block_button.html' with blocks=True %}
{% else %} {% else %}
<div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}"> <div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}">
<div class="control"> <div class="control">
<form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ user.id }} {% if request.user in user.followers.all or request.user in user.follower_requests.all %}is-hidden{%endif %}" data-id="follow_{{ user.id }}"> <form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ user.id }} {% if relationship.is_following or relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-small{% if not minimal %} is-link{% endif %}" type="submit"> <button class="button is-small{% if not minimal %} is-link{% endif %}" type="submit">
@ -18,10 +23,10 @@
{% endif %} {% endif %}
</button> </button>
</form> </form>
<form action="{% url 'unfollow' %}" method="POST" class="interaction follow_{{ user.id }} {% if not request.user in user.followers.all and not request.user in user.follower_requests.all %}is-hidden{%endif %}" data-id="follow_{{ user.id }}"> <form action="{% url 'unfollow' %}" method="POST" class="interaction follow_{{ user.id }} {% if not relationship.is_following and not relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
{% if user.manually_approves_followers and request.user not in user.followers.all %} {% if user.manually_approves_followers and not relationship.is_following %}
<button class="button is-small is-danger is-light" type="submit"> <button class="button is-small is-danger is-light" type="submit">
{% trans "Undo follow request" %} {% trans "Undo follow request" %}
</button> </button>
@ -42,4 +47,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% endif %} {% endif %}

View file

@ -25,10 +25,7 @@
<button class="button is-fullwidth is-small shelf-option is-radiusless is-white" type="submit" {% if shelf.identifier == current.identifier %}disabled{% endif %}> <button class="button is-fullwidth is-small shelf-option is-radiusless is-white" type="submit" {% if shelf.identifier == current.identifier %}disabled{% endif %}>
<span> <span>
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %} {% include "snippets/translated_shelf_name.html" with shelf=shelf %}
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
{% else %}{{ shelf.name }}{% endif %}
</span> </span>
</button> </button>
</form> </form>
@ -41,20 +38,23 @@
{% trans "Start reading" as button_text %} {% trans "Start reading" as button_text %}
{% url 'reading-status' 'start' book.id as fallback_url %} {% url 'reading-status' 'start' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=button_class text=button_text controls_text="start_reading" controls_uid=uuid focus="modal_title_start_reading" disabled=is_current fallback_url=fallback_url %} {% join "start_reading" uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
{% elif shelf.identifier == 'read' %} {% elif shelf.identifier == 'read' %}
{% trans "Read" as button_text %} {% trans "Read" as button_text %}
{% url 'reading-status' 'finish' book.id as fallback_url %} {% url 'reading-status' 'finish' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=button_class text=button_text controls_text="finish_reading" controls_uid=uuid focus="modal_title_finish_reading" disabled=is_current fallback_url=fallback_url %} {% join "finish_reading" uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
{% elif shelf.identifier == 'to-read' %} {% elif shelf.identifier == 'to-read' %}
{% trans "Want to read" as button_text %} {% trans "Want to read" as button_text %}
{% url 'reading-status' 'want' book.id as fallback_url %} {% url 'reading-status' 'want' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=button_class text=button_text controls_text="want_to_read" controls_uid=uuid focus="modal_title_want_to_read" disabled=is_current fallback_url=fallback_url %} {% join "want_to_read" uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
{% endif %} {% endif %}
</li> </li>
@ -95,7 +95,7 @@
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id refresh=True class="" %} {% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id refresh=True class="" %}
{% join "finish_reading" uuid as modal_id %} {% join "finish_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %} {% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,12 @@
{% load i18n %}
{% if shelf.identifier == 'all' %}
{% trans "All books" %}
{% elif shelf.identifier == 'to-read' %}
{% trans "To Read" %}
{% elif shelf.identifier == 'reading' %}
{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}
{% trans "Read" %}
{% else %}
{{ shelf.name }}
{% endif %}

View file

@ -1,16 +1,10 @@
{% load i18n %} {% load i18n %}
{% if is_self %} {% if is_self %}
{% if shelf.identifier == 'to-read' %} {% if shelf.identifier == 'all' %}
{% trans "To Read" %} {% trans "Your books" %}
{% elif shelf.identifier == 'reading' %} {% else %}
{% trans "Currently Reading" %} {% include "snippets/translated_shelf_name.html" with shelf=shelf %}
{% elif shelf.identifier == 'read' %} {% endif %}
{% trans "Read" %}
{% elif shelf.identifier == 'all' %}
{% trans "Your books" %}
{% else %} {% else %}
{{ shelf.name }} {% blocktrans with username=user.display_name %}{{ username }}'s books{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans with username=user.display_name %}{{ username }}'s books{% endblocktrans %}
{% endif %} {% endif %}

View file

@ -3,6 +3,7 @@ from django import template
from django.db.models import Avg, StdDev, Count, F, Q from django.db.models import Avg, StdDev, Count, F, Q
from bookwyrm import models from bookwyrm import models
from bookwyrm.utils import cache
from bookwyrm.views.feed import get_suggested_books from bookwyrm.views.feed import get_suggested_books
@ -130,35 +131,55 @@ def related_status(notification):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def active_shelf(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"""
if hasattr(book, "current_shelves"): user = context["request"].user
read_shelves = [ return (
s cache.get_or_set(
for s in book.current_shelves f"active_shelf-{user.id}-{book.id}",
if s.shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS lambda u, b: (
] models.ShelfBook.objects.filter(
return read_shelves[0] if len(read_shelves) else {"book": book} shelf__user=u,
book__parent_work__editions=b,
shelf = ( ).first()
models.ShelfBook.objects.filter( ),
shelf__user=context["request"].user, user,
book__parent_work__editions=book, book,
timeout=15552000,
) )
.select_related("book", "shelf") or {"book": book}
.first()
) )
return shelf if shelf else {"book": book}
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def latest_read_through(book, user): def latest_read_through(book, user):
"""the most recent read activity""" """the most recent read activity"""
if hasattr(book, "active_readthroughs"): return cache.get_or_set(
return book.active_readthroughs[0] if len(book.active_readthroughs) else None f"latest_read_through-{user.id}-{book.id}",
lambda u, b: (
models.ReadThrough.objects.filter(user=u, book=b, is_active=True)
.order_by("-start_date")
.first()
),
user,
book,
timeout=15552000,
)
return (
models.ReadThrough.objects.filter(user=user, book=book, is_active=True) @register.simple_tag(takes_context=False)
.order_by("-start_date") def get_landing_books():
.first() """list of books for the landing page"""
return list(
set(
models.Edition.objects.filter(
review__published_date__isnull=False,
review__deleted=False,
review__user__local=True,
review__privacy__in=["public", "unlisted"],
)
.exclude(cover__exact="")
.distinct()
.order_by("-review__published_date")[:6]
)
) )

View file

@ -1,8 +1,8 @@
""" template filters for status interaction buttons """ """ template filters for status interaction buttons """
from django import template from django import template
from django.core.cache import cache
from bookwyrm import models from bookwyrm import models
from bookwyrm.utils.cache import get_or_set
register = template.Library() register = template.Library()
@ -11,20 +11,23 @@ register = template.Library()
@register.filter(name="liked") @register.filter(name="liked")
def get_user_liked(user, status): def get_user_liked(user, status):
"""did the given user fav a status?""" """did the given user fav a status?"""
return cache.get_or_set( return get_or_set(
f"fav-{user.id}-{status.id}", f"fav-{user.id}-{status.id}",
models.Favorite.objects.filter(user=user, status=status).exists(), lambda u, s: models.Favorite.objects.filter(user=u, status=s).exists(),
259200, user,
status,
timeout=259200,
) )
@register.filter(name="boosted") @register.filter(name="boosted")
def get_user_boosted(user, status): def get_user_boosted(user, status):
"""did the given user fav a status?""" """did the given user fav a status?"""
return cache.get_or_set( return get_or_set(
f"boost-{user.id}-{status.id}", f"boost-{user.id}-{status.id}",
status.boosters.filter(user=user).exists(), lambda u: status.boosters.filter(user=u).exists(),
259200, user,
timeout=259200,
) )
@ -32,3 +35,32 @@ def get_user_boosted(user, status):
def get_user_saved_lists(user, book_list): def get_user_saved_lists(user, book_list):
"""did the user save a list""" """did the user save a list"""
return user.saved_lists.filter(id=book_list.id).exists() return user.saved_lists.filter(id=book_list.id).exists()
@register.simple_tag(takes_context=True)
def get_relationship(context, user_object):
"""caches the relationship between the logged in user and another user"""
user = context["request"].user
return get_or_set(
f"relationship-{user.id}-{user_object.id}",
get_relationship_name,
user,
user_object,
timeout=259200,
)
def get_relationship_name(user, user_object):
"""returns the relationship type"""
types = {
"is_following": False,
"is_follow_pending": False,
"is_blocked": False,
}
if user_object in user.blocks.all():
types["is_blocked"] = True
elif user_object in user.following.all():
types["is_following"] = True
elif user_object in user.follower_requests.all():
types["is_follow_pending"] = True
return types

View file

@ -231,11 +231,12 @@ class ReadingViews(TestCase):
"finish_date": "2018-03-07", "finish_date": "2018-03-07",
"book": self.book.id, "book": self.book.id,
"id": "", "id": "",
"user": self.local_user.id,
}, },
) )
request.user = self.local_user request.user = self.local_user
views.create_readthrough(request) views.ReadThrough.as_view()(request)
readthrough = models.ReadThrough.objects.get() readthrough = models.ReadThrough.objects.get()
self.assertEqual(readthrough.start_date.year, 2017) self.assertEqual(readthrough.start_date.year, 2017)
self.assertEqual(readthrough.start_date.month, 1) self.assertEqual(readthrough.start_date.month, 1)

View file

@ -41,10 +41,8 @@ class ReadThrough(TestCase):
self.assertEqual(self.edition.readthrough_set.count(), 0) self.assertEqual(self.edition.readthrough_set.count(), 0)
self.client.post( self.client.post(
"/reading-status/start/{}".format(self.edition.id), f"/reading-status/start/{self.edition.id}",
{ {"start_date": "2020-11-27"},
"start_date": "2020-11-27",
},
) )
readthroughs = self.edition.readthrough_set.all() readthroughs = self.edition.readthrough_set.all()
@ -62,10 +60,8 @@ class ReadThrough(TestCase):
self.assertEqual(self.edition.readthrough_set.count(), 0) self.assertEqual(self.edition.readthrough_set.count(), 0)
self.client.post( self.client.post(
"/reading-status/start/{}".format(self.edition.id), f"/reading-status/start/{self.edition.id}",
{ {"start_date": "2020-11-27"},
"start_date": "2020-11-27",
},
) )
readthroughs = self.edition.readthrough_set.all() readthroughs = self.edition.readthrough_set.all()

View file

@ -500,7 +500,11 @@ urlpatterns = [
# reading progress # reading progress
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"), re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
re_path(r"^delete-readthrough/?$", views.delete_readthrough), re_path(r"^delete-readthrough/?$", views.delete_readthrough),
re_path(r"^create-readthrough/?$", views.create_readthrough), re_path(
r"^create-readthrough/?$",
views.ReadThrough.as_view(),
name="create-readthrough",
),
re_path(r"^delete-progressupdate/?$", views.delete_progressupdate), re_path(r"^delete-progressupdate/?$", views.delete_progressupdate),
# shelve actions # shelve actions
re_path( re_path(

11
bookwyrm/utils/cache.py Normal file
View file

@ -0,0 +1,11 @@
""" Custom handler for caching """
from django.core.cache import cache
def get_or_set(cache_key, function, *args, timeout=None):
"""Django's built-in get_or_set isn't cutting it"""
value = cache.get(cache_key)
if value is None:
value = function(*args)
cache.set(cache_key, value, timeout=timeout)
return value

View file

@ -94,7 +94,7 @@ from .list import Lists, SavedLists, List, Curate, UserLists
from .list import save_list, unsave_list, delete_list, unsafe_embed_list from .list import save_list, unsave_list, delete_list, unsafe_embed_list
from .notifications import Notifications from .notifications import Notifications
from .outbox import Outbox from .outbox import Outbox
from .reading import create_readthrough, delete_readthrough, delete_progressupdate from .reading import ReadThrough, delete_readthrough, delete_progressupdate
from .reading import ReadingStatus from .reading import ReadingStatus
from .report import Report from .report import Report
from .rss_feed import RssFeed from .rss_feed import RssFeed

View file

@ -16,7 +16,6 @@ from django.views.decorators.http import require_POST
from bookwyrm import emailing, forms, models from bookwyrm import emailing, forms, models
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views import helpers
# pylint: disable= no-self-use # pylint: disable= no-self-use
@ -174,7 +173,6 @@ class InviteRequest(View):
data = { data = {
"request_form": form, "request_form": form,
"request_received": received, "request_received": received,
"books": helpers.get_landing_books(),
} }
return TemplateResponse(request, "landing/landing.html", data) return TemplateResponse(request, "landing/landing.html", data)

View file

@ -153,24 +153,6 @@ def is_blocked(viewer, user):
return False return False
def get_landing_books():
"""list of books for the landing page"""
return list(
set(
models.Edition.objects.filter(
review__published_date__isnull=False,
review__deleted=False,
review__user__local=True,
review__privacy__in=["public", "unlisted"],
)
.exclude(cover__exact="")
.distinct()
.order_by("-review__published_date")[:6]
)
)
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime: def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
"""ensures that data is stored consistently in the UTC timezone""" """ensures that data is stored consistently in the UTC timezone"""
if not date_str: if not date_str:

View file

@ -3,7 +3,6 @@ from django.template.response import TemplateResponse
from django.views import View from django.views import View
from bookwyrm import forms from bookwyrm import forms
from bookwyrm.views import helpers
from bookwyrm.views.feed import Feed from bookwyrm.views.feed import Feed
@ -28,6 +27,5 @@ class Landing(View):
data = { data = {
"register_form": forms.RegisterForm(), "register_form": forms.RegisterForm(),
"request_form": forms.InviteRequestForm(), "request_form": forms.InviteRequestForm(),
"books": helpers.get_landing_books(),
} }
return TemplateResponse(request, "landing/landing.html", data) return TemplateResponse(request, "landing/landing.html", data)

View file

@ -1,7 +1,6 @@
""" the good stuff! the books! """ """ the good stuff! the books! """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.cache import cache from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.db import transaction from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
@ -10,16 +9,16 @@ 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 django.views.decorators.http import require_POST
from bookwyrm import models from bookwyrm import forms, models
from bookwyrm.views.shelf.shelf_actions import unshelve from bookwyrm.views.shelf.shelf_actions import unshelve
from .status import CreateStatus from .status import CreateStatus
from .helpers import get_edition, handle_reading_status, is_api_request from .helpers import get_edition, handle_reading_status, is_api_request
from .helpers import load_date_in_user_tz_as_utc from .helpers import load_date_in_user_tz_as_utc
@method_decorator(login_required, name="dispatch")
# pylint: disable=no-self-use # pylint: disable=no-self-use
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
@method_decorator(login_required, name="dispatch")
class ReadingStatus(View): class ReadingStatus(View):
"""consider reading a book""" """consider reading a book"""
@ -46,12 +45,10 @@ class ReadingStatus(View):
if not identifier: if not identifier:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# invalidate the template cache # invalidate related caches
cache_keys = [ cache.delete(
make_template_fragment_key("shelve_button", [request.user.id, book_id]), f"active_shelf-{request.user.id}-{book_id}",
make_template_fragment_key("suggested_books", [request.user.id]), )
]
cache.delete_many(cache_keys)
desired_shelf = get_object_or_404( desired_shelf = get_object_or_404(
models.Shelf, identifier=identifier, user=request.user models.Shelf, identifier=identifier, user=request.user
@ -118,6 +115,45 @@ class ReadingStatus(View):
return redirect(referer) return redirect(referer)
@method_decorator(login_required, name="dispatch")
class ReadThrough(View):
"""Add new read dates"""
def get(self, request, book_id, readthrough_id=None):
"""standalone form in case of errors"""
book = get_object_or_404(models.Edition, id=book_id)
form = forms.ReadThroughForm()
data = {"form": form, "book": book}
if readthrough_id:
data["readthrough"] = get_object_or_404(
models.ReadThrough, id=readthrough_id
)
return TemplateResponse(request, "readthrough/readthrough.html", data)
def post(self, request):
"""can't use the form normally because the dates are too finnicky"""
book_id = request.POST.get("book")
normalized_post = request.POST.copy()
normalized_post["start_date"] = load_date_in_user_tz_as_utc(
request.POST.get("start_date"), request.user
)
normalized_post["finish_date"] = load_date_in_user_tz_as_utc(
request.POST.get("finish_date"), request.user
)
form = forms.ReadThroughForm(request.POST)
if not form.is_valid():
book = get_object_or_404(models.Edition, id=book_id)
data = {"form": form, "book": book}
if request.POST.get("id"):
data["readthrough"] = get_object_or_404(
models.ReadThrough, id=request.POST.get("id")
)
return TemplateResponse(request, "readthrough/readthrough.html", data)
form.save()
return redirect("book", book_id)
@transaction.atomic @transaction.atomic
def update_readthrough_on_shelve( def update_readthrough_on_shelve(
user, annotated_book, status, start_date=None, finish_date=None user, annotated_book, status, start_date=None, finish_date=None
@ -159,27 +195,6 @@ def delete_readthrough(request):
return redirect(request.headers.get("Referer", "/")) return redirect(request.headers.get("Referer", "/"))
@login_required
@require_POST
def create_readthrough(request):
"""can't use the form because the dates are too finnicky"""
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
start_date = load_date_in_user_tz_as_utc(
request.POST.get("start_date"), request.user
)
finish_date = load_date_in_user_tz_as_utc(
request.POST.get("finish_date"), request.user
)
models.ReadThrough.objects.create(
user=request.user,
book=book,
start_date=start_date,
finish_date=finish_date,
)
return redirect("book", book.id)
@login_required @login_required
@require_POST @require_POST
def delete_progressupdate(request): def delete_progressupdate(request):

View file

@ -159,6 +159,7 @@ def update_progress(request, book_id): # pylint: disable=unused-argument
@require_POST @require_POST
def edit_readthrough(request): def edit_readthrough(request):
"""can't use the form because the dates are too finnicky""" """can't use the form because the dates are too finnicky"""
# TODO: remove this, it duplicates the code in the ReadThrough view
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id")) readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
readthrough.raise_not_editable(request.user) readthrough.raise_not_editable(request.user)

View file

@ -6,13 +6,18 @@ markers =
integration: marks tests as requiring external resources (deselect with '-m "not integration"') integration: marks tests as requiring external resources (deselect with '-m "not integration"')
env = env =
SECRET_KEY = beepbeep
DEBUG = false DEBUG = false
USE_HTTPS=true USE_HTTPS = true
DOMAIN = your.domain.here DOMAIN = your.domain.here
BOOKWYRM_DATABASE_BACKEND = postgres BOOKWYRM_DATABASE_BACKEND = postgres
MEDIA_ROOT = images/ MEDIA_ROOT = images/
CELERY_BROKER = "" CELERY_BROKER = ""
REDIS_BROKER_PORT = 6379 REDIS_BROKER_PORT = 6379
REDIS_BROKER_PASSWORD = beep
REDIS_ACTIVITY_PORT = 6379
REDIS_ACTIVITY_PASSWORD = beep
USE_DUMMY_CACHE = true
FLOWER_PORT = 8888 FLOWER_PORT = 8888
EMAIL_HOST = "smtp.mailgun.org" EMAIL_HOST = "smtp.mailgun.org"
EMAIL_PORT = 587 EMAIL_PORT = 587
@ -20,4 +25,3 @@ env =
EMAIL_HOST_PASSWORD = "" EMAIL_HOST_PASSWORD = ""
EMAIL_USE_TLS = true EMAIL_USE_TLS = true
ENABLE_PREVIEW_IMAGES = false ENABLE_PREVIEW_IMAGES = false
USE_S3 = false