From 66ee5ccacfe1d0b8bd97ad89779dcb3afb95ca53 Mon Sep 17 00:00:00 2001 From: Joachim Date: Tue, 21 Dec 2021 17:24:15 +0100 Subject: [PATCH 01/19] Fix tests? --- bookwyrm/tests/views/test_annual_summary.py | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/bookwyrm/tests/views/test_annual_summary.py b/bookwyrm/tests/views/test_annual_summary.py index e50731c7..d191395c 100644 --- a/bookwyrm/tests/views/test_annual_summary.py +++ b/bookwyrm/tests/views/test_annual_summary.py @@ -12,6 +12,11 @@ from bookwyrm import models, views from bookwyrm.tests.validate_html import validate_html +def make_date(*args): + """helper function to easily generate a date obj""" + return datetime(*args, tzinfo=pytz.UTC) + + class AnnualSummary(TestCase): """views""" @@ -91,18 +96,18 @@ class AnnualSummary(TestCase): validate_html(result.render()) self.assertEqual(result.status_code, 200) + @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") + @patch("bookwyrm.activitystreams.add_book_statuses_task.delay") def test_annual_summary_page(self, *_): """there are so many views, this just makes sure it LOADS""" - shelf = self.local_user.shelf_set.get(identifier="read") - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"): - models.ShelfBook.objects.create( - book=self.book, - user=self.local_user, - shelf=shelf, - shelved_date=datetime(2020, 1, 1), - ) + shelf = self.local_user.shelf_set.filter(identifier="read").first() + models.ShelfBook.objects.create( + book=self.book, + user=self.local_user, + shelf=shelf, + shelved_date=make_date(2020, 1, 1), + ) view = views.AnnualSummary.as_view() request = self.factory.get("") From 67092fd3e32538bc7c513da1b0e50689c9e8c25a Mon Sep 17 00:00:00 2001 From: Joachim Date: Tue, 21 Dec 2021 17:35:57 +0100 Subject: [PATCH 02/19] :facepalm: --- bookwyrm/tests/views/test_annual_summary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bookwyrm/tests/views/test_annual_summary.py b/bookwyrm/tests/views/test_annual_summary.py index d191395c..2245adbc 100644 --- a/bookwyrm/tests/views/test_annual_summary.py +++ b/bookwyrm/tests/views/test_annual_summary.py @@ -1,5 +1,6 @@ """testing the annual summary page""" from datetime import datetime +import pytz from unittest.mock import patch from django.contrib.auth.models import AnonymousUser From 0da0a626609794c9bed84dac823e151de6cc4728 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 11:18:26 +0100 Subject: [PATCH 03/19] Don't crash if there's no ratings --- bookwyrm/views/annual_summary.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index 26374ea9..cfdd6093 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -105,7 +105,9 @@ class AnnualSummary(View): "book_pages_highest": book_list_by_pages.last(), "no_page_number": no_page_list, "ratings_total": len(ratings), - "rating_average": round(ratings_stats["rating__avg"], 2), + "rating_average": round( + ratings_stats["rating__avg"] if ratings_stats["rating__avg"] else 0, 2 + ), "book_rating_highest": ratings.order_by("-rating").first(), "best_ratings_books_ids": [ review.book.id for review in ratings.filter(rating=5) From ce9c6f1727014be3336150194506eabbb5cbedbf Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 14:53:34 +0100 Subject: [PATCH 04/19] Color links in green --- bookwyrm/templates/annual_summary/layout.html | 16 ++++++++-------- bookwyrm/templates/snippets/authors.html | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html index f994e978..99c5343d 100644 --- a/bookwyrm/templates/annual_summary/layout.html +++ b/bookwyrm/templates/annual_summary/layout.html @@ -67,13 +67,13 @@
{% trans "Your shortest read this year" %}

- + {{ book_pages_lowest.title }}

{% if book_pages_lowest.authors.exists %}

{% trans "by" %} - {% include 'snippets/authors.html' with book=book_pages_lowest %} + {% include 'snippets/authors.html' with book=book_pages_lowest link_class="has-text-success-dark" %}

{% endif %}

@@ -88,13 +88,13 @@

{% trans "and the longest read" %}

- + {{ book_pages_highest.title }}

{% if book_pages_highest.authors.exists %}

{% trans "by" %} - {% include 'snippets/authors.html' with book=book_pages_highest %} + {% include 'snippets/authors.html' with book=book_pages_highest link_class="has-text-success-dark" %}

{% endif %}

@@ -126,13 +126,13 @@

{% trans "Your best rated review" %}

- + {{ book_rating_highest.book.title }}

{% if book_rating_highest.book.authors.exists %}

{% trans "by" %} - {% include 'snippets/authors.html' with book=book_rating_highest.book %} + {% include 'snippets/authors.html' with book=book_rating_highest.book link_class="has-text-success-dark" %}

{% endif %}

@@ -162,14 +162,14 @@

{% for book in books %} {% if book.id in best_ratings_books_ids %} - + {% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' %} {{ book.title }} {% else %} - + {% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' %} {{ book.title }} diff --git a/bookwyrm/templates/snippets/authors.html b/bookwyrm/templates/snippets/authors.html index 81aaa138..69136adf 100644 --- a/bookwyrm/templates/snippets/authors.html +++ b/bookwyrm/templates/snippets/authors.html @@ -13,7 +13,7 @@ {% for author in book.authors.all|slice:limit %} +{% endif %} -

+

📚✨ {% blocktrans %}{{ year }} in the books{% endblocktrans %} ✨📚

+

+ {% blocktrans %}{{ display_name }}’s year of reading{% endblocktrans %} +

{% if not books %}

{% blocktrans %}Sadly you didn't finish any book in {{ year }}{% endblocktrans %}

@@ -45,7 +51,7 @@

- {% blocktrans %}In {{ year }}, you read {{ books_total }} books
for a total of {{ pages_total }} pages!{% endblocktrans %} + {% blocktrans %}In {{ year }}, {{ display_name }} read {{ books_total }} books
for a total of {{ pages_total }} pages!{% endblocktrans %}

{% trans "That’s great!" %}

@@ -65,7 +71,7 @@ {% include 'snippets/book_cover.html' with book=book_pages_lowest cover_class='is-w-auto-tablet is-h-l-mobile' %}
- {% trans "and the longest read" %} + {% trans "…and the longest" %}

{{ book_pages_highest.title }} @@ -112,10 +118,11 @@

+ {% if ratings_total > 0 %}

- {% blocktrans %}You left {{ ratings_total }} ratings,
your average rating is {{ rating_average }}{% endblocktrans %} + {% blocktrans %}{{ display_name }} left {{ ratings_total }} ratings,
their average rating is {{ rating_average }}{% endblocktrans %}

@@ -124,7 +131,7 @@
{% include 'snippets/book_cover.html' with book=book_rating_highest.book cover_class='is-w-auto-tablet is-h-l-mobile' %}
@@ -148,11 +155,12 @@
+ {% endif %}

- {% blocktrans %}All the books you read in 2021{% endblocktrans %} + {% blocktrans %}All the books {{ display_name }} read in 2021{% endblocktrans %}

@@ -180,5 +188,6 @@
- {% endif %} +{% endif %} +{% endwith %} {% endblock %} diff --git a/bookwyrm/templates/feed/summary_card.html b/bookwyrm/templates/feed/summary_card.html index e726792e..a5bc4643 100644 --- a/bookwyrm/templates/feed/summary_card.html +++ b/bookwyrm/templates/feed/summary_card.html @@ -15,7 +15,7 @@

- + {% blocktrans %}Discover your stats for {{ year }}!{% endblocktrans %}

diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 7b7bc374..30e54bc7 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -479,7 +479,11 @@ urlpatterns = [ ), # annual summary re_path( - r"^my-year-in-the-books/(?P\d{4})/?$", + r"^my-year-in-the-books/(?P\d+)/?$", + views.personal_annual_summary, + ), + re_path( + rf"{LOCAL_USER_PATH}/(?P\d+)-in-the-books/?$", views.AnnualSummary.as_view(), name="annual-summary", ), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 30195fad..2eeda114 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -96,4 +96,7 @@ from .status import edit_readthrough from .updates import get_notification_count, get_unread_status_count from .user import User, Followers, Following, hide_suggestions from .wellknown import * -from .annual_summary import AnnualSummary +from .annual_summary import ( + AnnualSummary, + personal_annual_summary, +) diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index cfdd6093..5a98c009 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -1,13 +1,17 @@ """end-of-year read books stats""" from datetime import date +from uuid import uuid4 +from django.contrib.auth.decorators import login_required from django.db.models import Case, When, Avg, Sum from django.http import Http404 -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.views import View +from django.views.decorators.http import require_POST from bookwyrm import models +from .helpers import get_user_from_username # December day of first availability @@ -16,6 +20,100 @@ FIRST_DAY = 15 LAST_DAY = 15 +# pylint: disable= no-self-use +class AnnualSummary(View): + """display a summary of the year for the current user""" + + def get(self, request, username, year): + """get response""" + + if not is_year_available(year): + raise Http404(f"The summary for {year} is unavailable") + + paginated_years = ( + int(year) - 1, + int(year) + 1 if is_year_available(int(year) + 1) else None, + ) + + user = get_user_from_username(request.user, username) + + year_key = None + if user.summary_keys and year in user.summary_keys: + year_key = user.summary_keys[year] + + # verify key + if user != request.user: + request_key = None + if "key" in request.GET: + request_key = request.GET["key"] + + if not request_key or request_key != year_key: + raise Http404(f"The summary for {year} is unavailable") + + # get data + read_book_ids_in_year = get_read_book_ids_in_year(user, year) + + if len(read_book_ids_in_year) == 0: + data = { + "summary_user": user, + "year": year, + "book_total": 0, + "books": [], + "paginated_years": paginated_years, + } + return TemplateResponse(request, "annual_summary/layout.html", data) + + read_books_in_year = get_books_from_shelfbooks(read_book_ids_in_year) + + # pages stats queries + page_stats = read_books_in_year.aggregate(Sum("pages"), Avg("pages")) + book_list_by_pages = read_books_in_year.filter(pages__gte=0).order_by("pages") + + # books with no pages + no_page_list = len(read_books_in_year.filter(pages__exact=None)) + + # rating stats queries + ratings = ( + models.Review.objects.filter(user=user) + .exclude(deleted=True) + .exclude(rating=None) + .filter(book_id__in=read_book_ids_in_year) + ) + ratings_stats = ratings.aggregate(Avg("rating")) + + data = { + "summary_user": user, + "year": year, + "books_total": len(read_books_in_year), + "books": read_books_in_year, + "pages_total": page_stats["pages__sum"], + "pages_average": round( + page_stats["pages__avg"] if page_stats["pages__avg"] else 0 + ), + "book_pages_lowest": book_list_by_pages.first(), + "book_pages_highest": book_list_by_pages.last(), + "no_page_number": no_page_list, + "ratings_total": len(ratings), + "rating_average": round( + ratings_stats["rating__avg"] if ratings_stats["rating__avg"] else 0, 2 + ), + "book_rating_highest": ratings.order_by("-rating").first(), + "best_ratings_books_ids": [ + review.book.id for review in ratings.filter(rating=5) + ], + "paginated_years": paginated_years, + } + + return TemplateResponse(request, "annual_summary/layout.html", data) + + +@login_required +def personal_annual_summary(request, year): + """redirect simple URL to URL with username""" + + return redirect("annual-summary", request.user.localname, year) + + def get_annual_summary_year(): """return the latest available annual summary year or None""" @@ -44,80 +142,6 @@ def is_year_available(year): return False -# pylint: disable= no-self-use -class AnnualSummary(View): - """display a summary of the year for the current user""" - - def get(self, request, year): - """get response""" - - if not is_year_available(year): - raise Http404(f"The summary for {year} is unavailable") - - paginated_years = ( - int(year) - 1, - int(year) + 1 if is_year_available(int(year) + 1) else None, - ) - - user = request.user - - if not user.is_authenticated: - raise Http404(f"Login or register {year} to access this page") - - read_book_ids_in_year = get_read_book_ids_in_year(user, year) - - if len(read_book_ids_in_year) == 0: - data = { - "year": year, - "book_total": 0, - "books": [], - "paginated_years": paginated_years, - } - return TemplateResponse(request, "annual_summary/layout.html", data) - - read_books_in_year = get_books_from_shelfbooks(read_book_ids_in_year) - - # pages stats queries - page_stats = read_books_in_year.aggregate(Sum("pages"), Avg("pages")) - book_list_by_pages = read_books_in_year.filter(pages__gte=0).order_by("pages") - - # books with no pages - no_page_list = len(read_books_in_year.filter(pages__exact=None)) - - # rating stats queries - ratings = ( - models.Review.objects.filter(user=user) - .exclude(deleted=True) - .exclude(rating=None) - .filter(book_id__in=read_book_ids_in_year) - ) - ratings_stats = ratings.aggregate(Avg("rating")) - - data = { - "year": year, - "books_total": len(read_books_in_year), - "books": read_books_in_year, - "pages_total": page_stats["pages__sum"], - "pages_average": round( - page_stats["pages__avg"] if page_stats["pages__avg"] else 0 - ), - "book_pages_lowest": book_list_by_pages.first(), - "book_pages_highest": book_list_by_pages.last(), - "no_page_number": no_page_list, - "ratings_total": len(ratings), - "rating_average": round( - ratings_stats["rating__avg"] if ratings_stats["rating__avg"] else 0, 2 - ), - "book_rating_highest": ratings.order_by("-rating").first(), - "best_ratings_books_ids": [ - review.book.id for review in ratings.filter(rating=5) - ], - "paginated_years": paginated_years, - } - - return TemplateResponse(request, "annual_summary/layout.html", data) - - def get_read_book_ids_in_year(user, year): """return an ordered QuerySet of the read book ids""" From af9c983145171a02928978d0004079b718dfb33a Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 15:11:55 +0100 Subject: [PATCH 07/19] Change voice for no read book --- bookwyrm/templates/annual_summary/layout.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html index f20a8f00..46f9c0b3 100644 --- a/bookwyrm/templates/annual_summary/layout.html +++ b/bookwyrm/templates/annual_summary/layout.html @@ -44,9 +44,9 @@ {% blocktrans %}{{ display_name }}’s year of reading{% endblocktrans %}

- {% if not books %} -

{% blocktrans %}Sadly you didn't finish any book in {{ year }}{% endblocktrans %}

- {% else %} +{% if not books %} +

{% blocktrans %}Sadly {{ display_name }} didn’t finish any book in {{ year }}{% endblocktrans %}

+{% else %}
From 45dd39d370f3b371103fb2ef993ebb41e8904b2d Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 15:12:56 +0100 Subject: [PATCH 08/19] Add key creation/revocation --- bookwyrm/templates/annual_summary/layout.html | 45 +++++++++++++++++++ bookwyrm/urls.py | 4 ++ bookwyrm/views/__init__.py | 2 + bookwyrm/views/annual_summary.py | 42 +++++++++++++++++ 4 files changed, 93 insertions(+) diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html index 46f9c0b3..0b46e725 100644 --- a/bookwyrm/templates/annual_summary/layout.html +++ b/bookwyrm/templates/annual_summary/layout.html @@ -44,6 +44,51 @@ {% blocktrans %}{{ display_name }}’s year of reading{% endblocktrans %}

+
+ +

Share this page

+
+
+
+ + {% if user == summary_user %} + {% if year_key %} +
+
+

Sharing status: public with key

+

The page can be seen by anyone with the complete address.

+
+
+ {% csrf_token %} + + +
+
+ {% else %} +
+
+

Sharing status: private

+

The page is private, only you can see it.

+
+
+ {% csrf_token %} + + +
+
+ {% endif %} +

When you make your page private, the old key won’t give access to the page anymore. A new key will be created if the page is once again made public.

+ {% endif %} +
+
+
+ +
+
+
+
+
+ {% if not books %}

{% blocktrans %}Sadly {{ display_name }} didn’t finish any book in {{ year }}{% endblocktrans %}

{% else %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 30e54bc7..7220b545 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -487,4 +487,8 @@ urlpatterns = [ views.AnnualSummary.as_view(), name="annual-summary", ), + re_path(r"^summary_add_key/?$", views.summary_add_key, name="summary-add-key"), + re_path( + r"^summary_revoke_key/?$", views.summary_revoke_key, name="summary-revoke-key" + ), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 2eeda114..8b1f5648 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -99,4 +99,6 @@ from .wellknown import * from .annual_summary import ( AnnualSummary, personal_annual_summary, + summary_add_key, + summary_revoke_key, ) diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index 5a98c009..03d724c0 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -57,6 +57,7 @@ class AnnualSummary(View): data = { "summary_user": user, "year": year, + "year_key": year_key, "book_total": 0, "books": [], "paginated_years": paginated_years, @@ -84,6 +85,7 @@ class AnnualSummary(View): data = { "summary_user": user, "year": year, + "year_key": year_key, "books_total": len(read_books_in_year), "books": read_books_in_year, "pages_total": page_stats["pages__sum"], @@ -114,6 +116,46 @@ def personal_annual_summary(request, year): return redirect("annual-summary", request.user.localname, year) +@login_required +@require_POST +def summary_add_key(request): + """add summary key""" + + year = request.POST["year"] + user = request.user + + new_key = uuid4().hex + + if not user.summary_keys: + user.summary_keys = { + year: new_key, + } + else: + user.summary_keys[year] = new_key + + user.save() + + response = redirect("annual-summary", user.localname, year) + response["Location"] += f"?key={str(new_key)}" + return response + + +@login_required +@require_POST +def summary_revoke_key(request): + """revoke summary key""" + + year = request.POST["year"] + user = request.user + + if user.summary_keys and year in user.summary_keys: + user.summary_keys.pop(year) + + user.save() + + return redirect("annual-summary", user.localname, year) + + def get_annual_summary_year(): """return the latest available annual summary year or None""" From 8de2bca2c530da5872d63cb8575f2b2847eac524 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 15:13:42 +0100 Subject: [PATCH 09/19] Add + adjust text copy component --- bookwyrm/static/css/bookwyrm.css | 35 +++++++++++++++++++ bookwyrm/static/js/bookwyrm.js | 2 -- bookwyrm/templates/annual_summary/layout.html | 6 ++++ bookwyrm/templates/lists/list.html | 4 ++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index b63ac072..19a7cac8 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -136,6 +136,10 @@ summary::marker { content: none; } +summary { + cursor: pointer; +} + .detail-pinned-button summary { position: absolute; right: 0; @@ -586,6 +590,37 @@ ol.ordered-list li::before { min-height: calc(2 * var(--height-basis)); } +/* Copy + ******************************************************************************/ + +.horizontal-copy { + display: flex; + flex-direction: row; + align-items: center; + gap: .75rem; +} + +.horizontal-copy textarea { + min-width: initial; + white-space: nowrap; +} + +.horizontal-copy button { + align-self: stretch; + height: unset; +} + +.vertical-copy { + display: flex; + flex-direction: column; + align-items: stretch; + gap: .75rem; +} + +.vertical-copy button { + width: 100%; +} + /* Dimensions * @todo These could be in rem. ******************************************************************************/ diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 2b78bf51..2e446a85 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -465,10 +465,8 @@ let BookWyrm = new class { copyButtonEl.textContent = textareaEl.dataset.copytextLabel; copyButtonEl.classList.add( - "mt-2", "button", "is-small", - "is-fullwidth", "is-primary", "is-light" ); diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html index 0b46e725..4b757224 100644 --- a/bookwyrm/templates/annual_summary/layout.html +++ b/bookwyrm/templates/annual_summary/layout.html @@ -51,6 +51,12 @@
+ {% if year_key %} +
+ +
+ {% endif %} + {% if user == summary_user %} {% if year_key %}
diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index 412ca470..836ca864 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -190,7 +190,9 @@

{% trans "Embed this list on a website" %}

- +
+ +
From 357eddf16e443eda210ac1f863c04ca3f5164e10 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 16:52:42 +0100 Subject: [PATCH 10/19] Limit page availability to earliest completed readthrough / shelving --- bookwyrm/templates/annual_summary/layout.html | 2 + bookwyrm/views/annual_summary.py | 62 ++++++++++++++++--- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html index 4b757224..b4089e8b 100644 --- a/bookwyrm/templates/annual_summary/layout.html +++ b/bookwyrm/templates/annual_summary/layout.html @@ -14,12 +14,14 @@ {% if user == summary_user %}
{% with year=paginated_years|first %} + {% if year %} + {% endif %} {% endwith %} {% with year=paginated_years|last %} diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index 03d724c0..b63c30a0 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -27,14 +27,6 @@ class AnnualSummary(View): def get(self, request, username, year): """get response""" - if not is_year_available(year): - raise Http404(f"The summary for {year} is unavailable") - - paginated_years = ( - int(year) - 1, - int(year) + 1 if is_year_available(int(year) + 1) else None, - ) - user = get_user_from_username(request.user, username) year_key = None @@ -50,6 +42,14 @@ class AnnualSummary(View): if not request_key or request_key != year_key: raise Http404(f"The summary for {year} is unavailable") + if not is_year_available(user, year): + raise Http404(f"The summary for {year} is unavailable") + + paginated_years = ( + int(year) - 1 if is_year_available(user, int(year) - 1) else None, + int(year) + 1 if is_year_available(user, int(year) + 1) else None, + ) + # get data read_book_ids_in_year = get_read_book_ids_in_year(user, year) @@ -171,12 +171,13 @@ def get_annual_summary_year(): return None -def is_year_available(year): +def is_year_available(user, year): """return boolean""" + earliest_year = get_earliest_year(user) today = date.today() year = int(year) - if year < today.year: + if year < today.year and year >= earliest_year: return True if year == today.year and today >= date(today.year, 12, FIRST_DAY): return True @@ -184,6 +185,36 @@ def is_year_available(year): return False +def get_earliest_year(user): + """return the earliest finish_date or shelved_date year for user books in read shelf""" + + read_shelfbooks = models.ShelfBook.objects.filter(user__id=user.id).filter( + shelf__identifier__exact="read" + ) + read_shelfbooks_list = list(read_shelfbooks.values("book", "shelved_date")) + + book_dates = [] + + for book in read_shelfbooks_list: + earliest_finished = ( + models.ReadThrough.objects.filter(user__id=user.id) + .filter(book_id=book["book"]) + .exclude(finish_date__exact=None) + .order_by("finish_date") + .values("finish_date") + .first() + ) + + if earliest_finished: + book_dates.append( + min(earliest_finished["finish_date"], book["shelved_date"]) + ) + else: + book_dates.append(book["shelved_date"]) + + return min(book_dates).year + + def get_read_book_ids_in_year(user, year): """return an ordered QuerySet of the read book ids""" @@ -196,6 +227,17 @@ def get_read_book_ids_in_year(user, year): .values_list("book", flat=True) ) return read_book_ids_in_year + models.ReadThrough.objects.filter(user__id=user.id) + .filter(book_id=book[0]) + .exists() + ) + if not has_other_year_readthrough and book[1].year == int(year): + # No readthrough but shelved this year + book_dates.append(book) + + book_dates = sorted(book_dates, key=lambda tup: tup[1]) + + return [book[0] for book in book_dates] def get_books_from_shelfbooks(books_ids): From 13ee7e7a65e685fe68a2f1d9b5050b14f34e4ca2 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 16:53:10 +0100 Subject: [PATCH 11/19] Use readthrough finish date instead of shelving date --- bookwyrm/views/annual_summary.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index b63c30a0..2ab99d23 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -219,14 +219,28 @@ def get_read_book_ids_in_year(user, year): """return an ordered QuerySet of the read book ids""" read_shelf = get_object_or_404(user.shelf_set, identifier="read") - read_book_ids_in_year = ( + shelved_book_ids = ( models.ShelfBook.objects.filter(shelf=read_shelf) .filter(user=user) - .filter(shelved_date__year=year) - .order_by("shelved_date", "created_date", "updated_date") - .values_list("book", flat=True) + .values_list("book", "shelved_date") ) - return read_book_ids_in_year + + book_dates = [] + + for book in shelved_book_ids: + finished_in_year = ( + models.ReadThrough.objects.filter(user__id=user.id) + .filter(book_id=book[0]) + .filter(finish_date__year=year) + .values("finish_date") + .first() + ) + + if finished_in_year: + # Finished a readthrough in the year + book_dates.append((book[0], finished_in_year["finish_date"])) + else: + has_other_year_readthrough = ( models.ReadThrough.objects.filter(user__id=user.id) .filter(book_id=book[0]) .exists() From d4b8aa51f698df69a53d6c54173de56711d76a33 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 16:55:02 +0100 Subject: [PATCH 12/19] lint styles --- bookwyrm/static/css/bookwyrm.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index 19a7cac8..43a59a62 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -594,10 +594,10 @@ ol.ordered-list li::before { ******************************************************************************/ .horizontal-copy { - display: flex; + display: flex; flex-direction: row; align-items: center; - gap: .75rem; + gap: 0.75rem; } .horizontal-copy textarea { @@ -611,10 +611,10 @@ ol.ordered-list li::before { } .vertical-copy { - display: flex; + display: flex; flex-direction: column; align-items: stretch; - gap: .75rem; + gap: 0.75rem; } .vertical-copy button { From 04d51cde3f9c434e7e6f9efe5453992168b93624 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 17:09:12 +0100 Subject: [PATCH 13/19] pylint --- bookwyrm/views/annual_summary.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index 2ab99d23..5c4b04ee 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -33,17 +33,7 @@ class AnnualSummary(View): if user.summary_keys and year in user.summary_keys: year_key = user.summary_keys[year] - # verify key - if user != request.user: - request_key = None - if "key" in request.GET: - request_key = request.GET["key"] - - if not request_key or request_key != year_key: - raise Http404(f"The summary for {year} is unavailable") - - if not is_year_available(user, year): - raise Http404(f"The summary for {year} is unavailable") + privacy_verification(request, user, year, year_key) paginated_years = ( int(year) - 1 if is_year_available(user, int(year) - 1) else None, @@ -171,13 +161,27 @@ def get_annual_summary_year(): return None +def privacy_verification(request, user, year, year_key): + if user != request.user: + request_key = None + if "key" in request.GET: + request_key = request.GET["key"] + + if not request_key or request_key != year_key: + raise Http404(f"The summary for {year} is unavailable") + + if not is_year_available(user, year): + raise Http404(f"The summary for {year} is unavailable") + + + def is_year_available(user, year): """return boolean""" earliest_year = get_earliest_year(user) today = date.today() year = int(year) - if year < today.year and year >= earliest_year: + if earliest_year <= year < today.year: return True if year == today.year and today >= date(today.year, 12, FIRST_DAY): return True From b9265bdd29daa150e9af54532309a926d9b00b12 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 17:10:47 +0100 Subject: [PATCH 14/19] Update annual_summary.py --- bookwyrm/views/annual_summary.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index 5c4b04ee..350090a6 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -174,7 +174,6 @@ def privacy_verification(request, user, year, year_key): raise Http404(f"The summary for {year} is unavailable") - def is_year_available(user, year): """return boolean""" From b03b6f6d6f0d0331227b016b41ecb71253d3064f Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 17:14:21 +0100 Subject: [PATCH 15/19] Add docstring --- bookwyrm/views/annual_summary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index 350090a6..eeecf2d7 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -162,6 +162,7 @@ def get_annual_summary_year(): def privacy_verification(request, user, year, year_key): + """raises a 404 error if the user should not access the page""" if user != request.user: request_key = None if "key" in request.GET: From 07f2d9a11c25f2c963cbb1af4dad9e86870dd4f0 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 18:15:05 +0100 Subject: [PATCH 16/19] Update tests --- bookwyrm/tests/views/test_annual_summary.py | 88 +++++++++++++-------- bookwyrm/views/annual_summary.py | 9 ++- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/bookwyrm/tests/views/test_annual_summary.py b/bookwyrm/tests/views/test_annual_summary.py index 2245adbc..d6254028 100644 --- a/bookwyrm/tests/views/test_annual_summary.py +++ b/bookwyrm/tests/views/test_annual_summary.py @@ -34,6 +34,7 @@ class AnnualSummary(TestCase): local=True, localname="mouse", remote_id="https://example.com/users/mouse", + summary_keys={"2020": "0123456789"}, ) self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( @@ -42,18 +43,11 @@ class AnnualSummary(TestCase): parent_work=self.work, pages=300, ) - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"): - self.review = models.Review.objects.create( - name="Review name", - content="test content", - rating=3.0, - user=self.local_user, - book=self.book, - ) + self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False - self.year = 2020 + self.year = "2020" models.SiteSettings.objects.create() def test_annual_summary_not_authenticated(self, *_): @@ -62,12 +56,24 @@ class AnnualSummary(TestCase): request = self.factory.get("") request.user = self.anonymous_user - with patch( - "bookwyrm.views.annual_summary.is_year_available" - ) as is_year_available: - is_year_available.return_value = True - with self.assertRaises(Http404): - view(request, self.year) + with self.assertRaises(Http404): + view(request, self.local_user.localname, self.year) + + def test_annual_summary_not_authenticated_with_key(self, *_): + """there are so many views, this just makes sure it DOES LOAD""" + key = self.local_user.summary_keys[self.year] + view = views.AnnualSummary.as_view() + request_url = ( + f"user/{self.local_user.localname}/{self.year}-in-the-books?key={key}" + ) + request = self.factory.get(request_url) + request.user = self.anonymous_user + + result = view(request, self.local_user.localname, self.year) + + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) def test_annual_summary_wrong_year(self, *_): """there are so many views, this just makes sure it DOESN’T LOAD""" @@ -75,12 +81,8 @@ class AnnualSummary(TestCase): request = self.factory.get("") request.user = self.anonymous_user - with patch( - "bookwyrm.views.annual_summary.is_year_available" - ) as is_year_available: - is_year_available.return_value = False - with self.assertRaises(Http404): - view(request, self.year) + with self.assertRaises(Http404): + view(request, self.local_user.localname, self.year) def test_annual_summary_empty_page(self, *_): """there are so many views, this just makes sure it LOADS""" @@ -88,11 +90,8 @@ class AnnualSummary(TestCase): request = self.factory.get("") request.user = self.local_user - with patch( - "bookwyrm.views.annual_summary.is_year_available" - ) as is_year_available: - is_year_available.return_value = True - result = view(request, self.year) + result = view(request, self.local_user.localname, self.year) + self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) self.assertEqual(result.status_code, 200) @@ -114,11 +113,38 @@ class AnnualSummary(TestCase): request = self.factory.get("") request.user = self.local_user - with patch( - "bookwyrm.views.annual_summary.is_year_available" - ) as is_year_available: - is_year_available.return_value = True - result = view(request, self.year) + result = view(request, self.local_user.localname, self.year) + + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + self.assertEqual(result.status_code, 200) + + @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") + @patch("bookwyrm.activitystreams.add_book_statuses_task.delay") + def test_annual_summary_page_with_review(self, *_): + """there are so many views, this just makes sure it LOADS""" + + self.review = models.Review.objects.create( + name="Review name", + content="test content", + rating=3.0, + user=self.local_user, + book=self.book, + ) + + shelf = self.local_user.shelf_set.filter(identifier="read").first() + models.ShelfBook.objects.create( + book=self.book, + user=self.local_user, + shelf=shelf, + shelved_date=make_date(2020, 1, 1), + ) + + view = views.AnnualSummary.as_view() + request = self.factory.get("") + request.user = self.local_user + + result = view(request, self.local_user.localname, self.year) self.assertIsInstance(result, TemplateResponse) validate_html(result.render()) diff --git a/bookwyrm/views/annual_summary.py b/bookwyrm/views/annual_summary.py index eeecf2d7..bfebf7f5 100644 --- a/bookwyrm/views/annual_summary.py +++ b/bookwyrm/views/annual_summary.py @@ -178,7 +178,7 @@ def privacy_verification(request, user, year, year_key): def is_year_available(user, year): """return boolean""" - earliest_year = get_earliest_year(user) + earliest_year = int(get_earliest_year(user, year)) today = date.today() year = int(year) if earliest_year <= year < today.year: @@ -189,7 +189,7 @@ def is_year_available(user, year): return False -def get_earliest_year(user): +def get_earliest_year(user, year): """return the earliest finish_date or shelved_date year for user books in read shelf""" read_shelfbooks = models.ShelfBook.objects.filter(user__id=user.id).filter( @@ -216,7 +216,10 @@ def get_earliest_year(user): else: book_dates.append(book["shelved_date"]) - return min(book_dates).year + if book_dates: + return min(book_dates).year + + return year def get_read_book_ids_in_year(user, year): From df7b40359a6ede2974ff9da4dfbbdc4f47c4ab96 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 18:15:20 +0100 Subject: [PATCH 17/19] Add translation calls --- bookwyrm/templates/annual_summary/layout.html | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html index b4089e8b..f9d289f7 100644 --- a/bookwyrm/templates/annual_summary/layout.html +++ b/bookwyrm/templates/annual_summary/layout.html @@ -48,7 +48,9 @@
-

Share this page

+ + {% trans "Share this page" %} +
@@ -63,29 +65,29 @@ {% if year_key %}
-

Sharing status: public with key

-

The page can be seen by anyone with the complete address.

+

{% trans "Sharing status: public with key" %}

+

{% trans "The page can be seen by anyone with the complete address." %}

{% csrf_token %} - +
{% else %}
-

Sharing status: private

-

The page is private, only you can see it.

+

{% trans "Sharing status: private" %}

+

{% trans "The page is private, only you can see it." %}

{% csrf_token %} - +
{% endif %} -

When you make your page private, the old key won’t give access to the page anymore. A new key will be created if the page is once again made public.

+

{% trans "When you make your page private, the old key won’t give access to the page anymore. A new key will be created if the page is once again made public." %}

{% endif %}
From 53146816418c8b1c90d42a569b5ecb6dcb84700d Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 22 Dec 2021 19:47:39 +0100 Subject: [PATCH 18/19] Update layout.html --- bookwyrm/templates/annual_summary/layout.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html index f9d289f7..7423d8e4 100644 --- a/bookwyrm/templates/annual_summary/layout.html +++ b/bookwyrm/templates/annual_summary/layout.html @@ -47,8 +47,8 @@

- - + + {% trans "Share this page" %} From 85486dcfad512b6d6f5d531f98c60b10fa167437 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 27 Dec 2021 13:29:47 -0800 Subject: [PATCH 19/19] Update bookwyrm.css --- bookwyrm/static/css/bookwyrm.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index 3c8386dc..4d9aabb4 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -125,6 +125,10 @@ input[type=file]::file-selector-button:hover { /** General `details` element styles ******************************************************************************/ +summary { + cursor: pointer; +} + summary::-webkit-details-marker { display: none; } @@ -133,10 +137,6 @@ summary::marker { content: none; } -summary { - cursor: pointer; -} - .detail-pinned-button summary { position: absolute; right: 0;