From 434cb7e5b5a969575583e45c3fd85fd04fdf2f0c Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 14:22:54 +0100 Subject: [PATCH 01/52] Create suggestion list --- bookwyrm/forms/lists.py | 6 +++++ bookwyrm/migrations/0173_suggestionlist.py | 26 ++++++++++++++++++++++ bookwyrm/models/__init__.py | 2 +- bookwyrm/models/fields.py | 2 +- bookwyrm/models/list.py | 8 +++++++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 bookwyrm/migrations/0173_suggestionlist.py diff --git a/bookwyrm/forms/lists.py b/bookwyrm/forms/lists.py index 647db3bfe9..945c2889df 100644 --- a/bookwyrm/forms/lists.py +++ b/bookwyrm/forms/lists.py @@ -14,6 +14,12 @@ class Meta: fields = ["user", "name", "description", "curation", "privacy", "group"] +class SuggestionListForm(CustomForm): + class Meta: + model = models.SuggestionList + fields = ["user", "book"] + + class ListItemForm(CustomForm): class Meta: model = models.ListItem diff --git a/bookwyrm/migrations/0173_suggestionlist.py b/bookwyrm/migrations/0173_suggestionlist.py new file mode 100644 index 0000000000..b056b3cb03 --- /dev/null +++ b/bookwyrm/migrations/0173_suggestionlist.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.16 on 2023-01-01 12:26 + +import bookwyrm.models.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0172_alter_user_preferred_language'), + ] + + operations = [ + migrations.CreateModel( + name='SuggestionList', + fields=[ + ('list_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.list')), + ('book', bookwyrm.models.fields.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.edition')), + ], + options={ + 'abstract': False, + }, + bases=('bookwyrm.list',), + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index ae70001623..ae2be0aff0 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -8,7 +8,7 @@ from .connector import Connector from .shelf import Shelf, ShelfBook -from .list import List, ListItem +from .list import List, SuggestionList, ListItem from .status import Status, GeneratedNote, Comment, Quotation from .status import Review, ReviewRating diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index d11f5fb1d1..dbced6d984 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -280,7 +280,7 @@ def field_to_activity(self, value): class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): - """activitypub-aware foreign key field""" + """activitypub-aware one to one field""" def field_to_activity(self, value): if not value: diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 63dd5b23f6..080bfd49c8 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -131,6 +131,14 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) +class SuggestionList(List): + """List related to a specific book""" + + book = fields.OneToOneField( + "Edition", on_delete=models.PROTECT, activitypub_field="book" + ) + + class ListItem(CollectionItemMixin, BookWyrmModel): """ok""" From acd379944d4ab3a842b57c2dade0bf49720b763c Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 14:23:37 +0100 Subject: [PATCH 02/52] Add list creation button --- bookwyrm/templates/book/book.html | 4 ++++ .../templates/book/suggestion_list/list.html | 11 ++++++++++ bookwyrm/urls.py | 5 +++++ bookwyrm/views/__init__.py | 1 + bookwyrm/views/books/books.py | 20 +++++++++++++++++++ 5 files changed, 41 insertions(+) create mode 100644 bookwyrm/templates/book/suggestion_list/list.html diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 6a8d4d794c..2a69829e56 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -344,6 +344,10 @@

{% trans "Your reading activity" %}

+
+ {% include "book/suggestion_list/list.html" %} +
+ {% if book.subjects %}

{% trans "Subjects" %}

diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html new file mode 100644 index 0000000000..8f9ad179ac --- /dev/null +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -0,0 +1,11 @@ +{% load i18n %} +{% if book.suggestionlist %} +

{% trans "Suggestions" %}

+{% else %} +
+ {% csrf_token %} + + + +
+{% endif %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index ac3a805803..7ee4bca8e0 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -664,6 +664,11 @@ views.update_book_from_remote, name="book-update-remote", ), + re_path( + rf"{BOOK_PATH}/create-suggestion-list/?$", + views.create_suggestion_list, + name="book-create-suggestion-list", + ), re_path( r"^author/(?P\d+)/update/(?P[\w\.]+)/?$", views.update_author_from_remote, diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index db88f1ae28..0e815853ec 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -49,6 +49,7 @@ upload_cover, add_description, resolve_book, + create_suggestion_list, ) from .books.books import update_book_from_remote from .books.edit_book import ( diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 565220b6ea..9339840694 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -208,3 +208,23 @@ def update_book_from_remote(request, book_id, connector_identifier): return Book().get(request, book_id, update_error=True) return redirect("book", book.id) + + +@login_required +@require_POST +def create_suggestion_list(request, book_id): + """create a suggestion_list""" + form = forms.SuggestionListForm(request.POST) + book = get_object_or_404(models.Edition, id=book_id) + + if not form.is_valid(): + return redirect("book", book.id) + suggestion_list = form.save(request, commit=False) + + # default values for the suggestion list + suggestion_list.privacy = "public" + suggestion_list.curation = "open" + suggestion_list.save() + + return redirect("book", book.id) + From fe05e319037b68eb433855c8514e69fa2714cbb6 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 14:27:54 +0100 Subject: [PATCH 03/52] Fix field doc --- bookwyrm/models/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index dbced6d984..572e4a3395 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -528,11 +528,11 @@ class BooleanField(ActivitypubFieldMixin, models.BooleanField): class IntegerField(ActivitypubFieldMixin, models.IntegerField): - """activitypub-aware boolean field""" + """activitypub-aware integer field""" class DecimalField(ActivitypubFieldMixin, models.DecimalField): - """activitypub-aware boolean field""" + """activitypub-aware decimal field""" def field_to_activity(self, value): if not value: From 25af64f03d88a0bf9880ea05f384d1717908b586 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 15:32:25 +0100 Subject: [PATCH 04/52] Display books in list --- bookwyrm/templates/book/book.html | 9 ++- .../templates/book/suggestion_list/list.html | 72 +++++++++++++++++-- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 2a69829e56..e920a496ca 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -344,10 +344,6 @@

{% trans "Your reading activity" %}

-
- {% include "book/suggestion_list/list.html" %} -
- {% if book.subjects %}

{% trans "Subjects" %}

@@ -408,8 +404,11 @@

{% trans "Lists" %}

- + +
+ {% include "book/suggestion_list/list.html" %} +
{% endwith %} {% endblock %} diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index 8f9ad179ac..d75e7c27a8 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -1,11 +1,69 @@ {% load i18n %} + +

+ {% trans "Suggestions" %} +

+ {% if book.suggestionlist %} -

{% trans "Suggestions" %}

+{% with book.suggestionlist.listitem_set.all as items %} + + {% if items.count == 0 %} +
+

+ {% trans "There are currently no suggestions." %} +
+

+

+ +

+
+ {% else %} +
    + {% for item in items %} +
  1. +
    +
    + {% with book=item.book %} + + +

    + {% include 'snippets/book_titleby.html' %} +

    + {% endwith %} +
    + +
    +
  2. + {% endfor %} +
+ {% endif %} +{% endwith %} {% else %} -
- {% csrf_token %} - - - -
+
+
+ {% csrf_token %} + + + +
+
{% endif %} From 4712673f945533e98708aba9cf8b414ff8cdccc6 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 16:48:19 +0100 Subject: [PATCH 05/52] Add book suggestion --- .../templates/book/suggestion_list/list.html | 25 ++++----- .../book/suggestion_list/search.html | 54 +++++++++++++++++++ bookwyrm/templates/lists/add_item_modal.html | 6 ++- bookwyrm/urls.py | 5 ++ bookwyrm/views/__init__.py | 1 + bookwyrm/views/books/books.py | 36 ++++++++++++- bookwyrm/views/list/list.py | 10 ++-- 7 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 bookwyrm/templates/book/suggestion_list/search.html diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index d75e7c27a8..cf97b53cde 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -1,23 +1,17 @@ {% load i18n %} -

+

{% trans "Suggestions" %}

{% if book.suggestionlist %} {% with book.suggestionlist.listitem_set.all as items %} - {% if items.count == 0 %} -
-

- {% trans "There are currently no suggestions." %} -
-

-

- -

+ {% if items|length == 0 %} +
+
+ {% include "book/suggestion_list/search.html" %} +
{% else %}
    @@ -28,7 +22,7 @@

    {% with book=item.book %} @@ -54,12 +48,15 @@

    {% endfor %} +
  1. + {% include "book/suggestion_list/search.html" %} +
{% endif %} {% endwith %} {% else %}
-
+ {% csrf_token %} diff --git a/bookwyrm/templates/book/suggestion_list/search.html b/bookwyrm/templates/book/suggestion_list/search.html new file mode 100644 index 0000000000..989583a0c6 --- /dev/null +++ b/bookwyrm/templates/book/suggestion_list/search.html @@ -0,0 +1,54 @@ +{% load i18n %} +{% load utilities %} + +{% if request.user.is_authenticated %} +{% with book.suggestionlist as list %} +

+ {% trans "Add suggestions" %} +

+ +
+
+ +
+
+ +
+
+ {% if query %} +

{% trans "Clear search" %}

+ {% endif %} +
+ {% if not suggested_books %} + {% if query %} +

{% blocktrans %}No books found matching the query "{{ query }}"{% endblocktrans %}

{% else %} +

{% trans "No books found" %}

+ {% endif %} + {% endif %} + + {% if suggested_books|length > 0 %} + {% for book in suggested_books %} +
+
+

{% include 'snippets/book_titleby.html' with book=book %}

+ + {% join "add_item" list.id book.id as modal_id %} + + {% include "lists/add_item_modal.html" with id=modal_id is_suggestion=True %} +
+
+ {% endfor %} + {% endif %} +{% endwith %} +{% endif %} + diff --git a/bookwyrm/templates/lists/add_item_modal.html b/bookwyrm/templates/lists/add_item_modal.html index 2c586b308c..184911cfca 100644 --- a/bookwyrm/templates/lists/add_item_modal.html +++ b/bookwyrm/templates/lists/add_item_modal.html @@ -19,7 +19,11 @@
{% endblock %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 7ee4bca8e0..6c5d4f993c 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -669,6 +669,11 @@ views.create_suggestion_list, name="book-create-suggestion-list", ), + re_path( + rf"{BOOK_PATH}/book-add-suggestion/?$", + views.book_add_suggestion, + name="book-add-suggestion", + ), re_path( r"^author/(?P\d+)/update/(?P[\w\.]+)/?$", views.update_author_from_remote, diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 0e815853ec..b35e833247 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -50,6 +50,7 @@ add_description, resolve_book, create_suggestion_list, + book_add_suggestion, ) from .books.books import update_book_from_remote from .books.edit_book import ( diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 9339840694..4d7ffecf92 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -3,7 +3,8 @@ from django.contrib.auth.decorators import login_required, permission_required from django.core.paginator import Paginator -from django.db.models import Avg, Q +from django.db import transaction +from django.db.models import Avg, Q, Max from django.http import Http404 from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse @@ -16,6 +17,7 @@ from bookwyrm.connectors.abstract_connector import get_image from bookwyrm.settings import PAGE_LENGTH from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path +from bookwyrm.views.list.list import get_list_suggestions, increment_order_in_reverse # pylint: disable=no-self-use @@ -74,6 +76,8 @@ def get(self, request, book_id, **kwargs): queryset = queryset.select_related("user").order_by("-published_date") paginated = Paginator(queryset, PAGE_LENGTH) + query = request.GET.get("suggestion_query", "") + lists = models.List.privacy_filter(request.user,).filter( listitem__approved=True, listitem__book__in=book.parent_work.editions.all(), @@ -90,6 +94,7 @@ def get(self, request, book_id, **kwargs): "rating": reviews.aggregate(Avg("rating"))["rating__avg"], "lists": lists, "update_error": kwargs.get("update_error", False), + "query": query, } if request.user.is_authenticated: @@ -122,6 +127,10 @@ def get(self, request, book_id, **kwargs): "comment_count": book.comment_set.filter(**filters).count(), "quotation_count": book.quotation_set.filter(**filters).count(), } + if hasattr(book, "suggestionlist"): + data["suggested_books"] = get_list_suggestions( + book.suggestionlist, request.user, query=query, ignore_id=book.id, + ) return TemplateResponse(request, "book/book.html", data) @@ -228,3 +237,28 @@ def create_suggestion_list(request, book_id): return redirect("book", book.id) + +@login_required +@require_POST +@transaction.atomic +def book_add_suggestion(request, book_id): + """put a book on the suggestion list""" + book_list = get_object_or_404(models.List, id=request.POST.get("book_list")) + + form = forms.ListItemForm(request.POST) + if not form.is_valid(): + return Book().get(request, book_id, add_failed=True) + + item = form.save(request, commit=False) + + # add the book at the latest order of approved books, before pending books + order_max = ( + book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[ + "order__max" + ] + ) or 0 + increment_order_in_reverse(book_list.id, order_max + 1) + item.order = order_max + 1 + item.save() + + return Book().get(request, book_id, add_succeeded=True) diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index 1adf7a6797..a8f31b5c05 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -94,22 +94,26 @@ def post(self, request, list_id): return redirect(book_list.local_path) -def get_list_suggestions(book_list, user, query=None): +def get_list_suggestions(book_list, user, query=None, ignore_id=None): """What books might a user want to add to a list""" if query: # search for books return book_search.search( query, - filters=[~Q(parent_work__editions__in=book_list.books.all())], + filters=[ + ~Q(parent_work__editions__in=book_list.books.all()), + ~Q(parent_work__editions__in=[ignore_id]), + ], ) # just suggest whatever books are nearby - suggestions = user.shelfbook_set.filter(~Q(book__in=book_list.books.all())) + suggestions = user.shelfbook_set.filter(~Q(book__in=book_list.books.all())).exclude(book__id=ignore_id) suggestions = [s.book for s in suggestions[:5]] if len(suggestions) < 5: suggestions += [ s.default_edition for s in models.Work.objects.filter( ~Q(editions__in=book_list.books.all()), + ~Q(editions__in=[ignore_id]), ).order_by("-updated_date")[: 5 - len(suggestions)] ] return suggestions From aee1af52fb4e5499a686d2ba076bf7bac23b0076 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 16:53:19 +0100 Subject: [PATCH 06/52] =?UTF-8?q?black=20=E2=9C=A8=20=F0=9F=8D=B0=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bookwyrm/migrations/0173_suggestionlist.py | 28 +++++++++++++++++----- bookwyrm/views/books/books.py | 7 ++++-- bookwyrm/views/list/list.py | 4 +++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/bookwyrm/migrations/0173_suggestionlist.py b/bookwyrm/migrations/0173_suggestionlist.py index b056b3cb03..96e9341e27 100644 --- a/bookwyrm/migrations/0173_suggestionlist.py +++ b/bookwyrm/migrations/0173_suggestionlist.py @@ -8,19 +8,35 @@ class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0172_alter_user_preferred_language'), + ("bookwyrm", "0172_alter_user_preferred_language"), ] operations = [ migrations.CreateModel( - name='SuggestionList', + name="SuggestionList", fields=[ - ('list_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.list')), - ('book', bookwyrm.models.fields.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.edition')), + ( + "list_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.list", + ), + ), + ( + "book", + bookwyrm.models.fields.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.edition", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.list',), + bases=("bookwyrm.list",), ), ] diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 4d7ffecf92..b577beeef1 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -129,7 +129,10 @@ def get(self, request, book_id, **kwargs): } if hasattr(book, "suggestionlist"): data["suggested_books"] = get_list_suggestions( - book.suggestionlist, request.user, query=query, ignore_id=book.id, + book.suggestionlist, + request.user, + query=query, + ignore_id=book.id, ) return TemplateResponse(request, "book/book.html", data) @@ -225,7 +228,7 @@ def create_suggestion_list(request, book_id): """create a suggestion_list""" form = forms.SuggestionListForm(request.POST) book = get_object_or_404(models.Edition, id=book_id) - + if not form.is_valid(): return redirect("book", book.id) suggestion_list = form.save(request, commit=False) diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index a8f31b5c05..61f870928b 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -106,7 +106,9 @@ def get_list_suggestions(book_list, user, query=None, ignore_id=None): ], ) # just suggest whatever books are nearby - suggestions = user.shelfbook_set.filter(~Q(book__in=book_list.books.all())).exclude(book__id=ignore_id) + suggestions = user.shelfbook_set.filter(~Q(book__in=book_list.books.all())).exclude( + book__id=ignore_id + ) suggestions = [s.book for s in suggestions[:5]] if len(suggestions) < 5: suggestions += [ From 3cb548f059878c8ae2a9ebbf491bc738b5956270 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 17:52:43 +0100 Subject: [PATCH 07/52] Switch from subclass to new column --- bookwyrm/forms/lists.py | 4 +- bookwyrm/migrations/0173_list_suggests_for.py | 26 ++++++++++++ bookwyrm/migrations/0173_suggestionlist.py | 42 ------------------- bookwyrm/models/__init__.py | 2 +- bookwyrm/models/list.py | 16 +++---- bookwyrm/views/books/books.py | 4 +- bookwyrm/views/list/lists.py | 11 ++++- 7 files changed, 48 insertions(+), 57 deletions(-) create mode 100644 bookwyrm/migrations/0173_list_suggests_for.py delete mode 100644 bookwyrm/migrations/0173_suggestionlist.py diff --git a/bookwyrm/forms/lists.py b/bookwyrm/forms/lists.py index 945c2889df..29c17bc0e5 100644 --- a/bookwyrm/forms/lists.py +++ b/bookwyrm/forms/lists.py @@ -16,8 +16,8 @@ class Meta: class SuggestionListForm(CustomForm): class Meta: - model = models.SuggestionList - fields = ["user", "book"] + model = models.List + fields = ["user", "suggests_for"] class ListItemForm(CustomForm): diff --git a/bookwyrm/migrations/0173_list_suggests_for.py b/bookwyrm/migrations/0173_list_suggests_for.py new file mode 100644 index 0000000000..48c9456967 --- /dev/null +++ b/bookwyrm/migrations/0173_list_suggests_for.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.16 on 2023-01-01 16:19 + +import bookwyrm.models.fields +from django.db import migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0172_alter_user_preferred_language"), + ] + + operations = [ + migrations.AddField( + model_name="list", + name="suggests_for", + field=bookwyrm.models.fields.OneToOneField( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="suggestion_list", + to="bookwyrm.edition", + ), + ), + ] diff --git a/bookwyrm/migrations/0173_suggestionlist.py b/bookwyrm/migrations/0173_suggestionlist.py deleted file mode 100644 index 96e9341e27..0000000000 --- a/bookwyrm/migrations/0173_suggestionlist.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 3.2.16 on 2023-01-01 12:26 - -import bookwyrm.models.fields -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("bookwyrm", "0172_alter_user_preferred_language"), - ] - - operations = [ - migrations.CreateModel( - name="SuggestionList", - fields=[ - ( - "list_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="bookwyrm.list", - ), - ), - ( - "book", - bookwyrm.models.fields.OneToOneField( - on_delete=django.db.models.deletion.PROTECT, - to="bookwyrm.edition", - ), - ), - ], - options={ - "abstract": False, - }, - bases=("bookwyrm.list",), - ), - ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index ae2be0aff0..ae70001623 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -8,7 +8,7 @@ from .connector import Connector from .shelf import Shelf, ShelfBook -from .list import List, SuggestionList, ListItem +from .list import List, ListItem from .status import Status, GeneratedNote, Comment, Quotation from .status import Review, ReviewRating diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 080bfd49c8..2c5e1e9779 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -47,6 +47,14 @@ class List(OrderedCollectionMixin, BookWyrmModel): ) embed_key = models.UUIDField(unique=True, null=True, editable=False) activity_serializer = activitypub.BookList + suggests_for = fields.OneToOneField( + "Edition", + on_delete=models.PROTECT, + activitypub_field="book", + related_name="suggestion_list", + default=None, + null=True, + ) def get_remote_id(self): """don't want the user to be in there in this case""" @@ -131,14 +139,6 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) -class SuggestionList(List): - """List related to a specific book""" - - book = fields.OneToOneField( - "Edition", on_delete=models.PROTECT, activitypub_field="book" - ) - - class ListItem(CollectionItemMixin, BookWyrmModel): """ok""" diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index b577beeef1..b55af8ea6b 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -127,9 +127,9 @@ def get(self, request, book_id, **kwargs): "comment_count": book.comment_set.filter(**filters).count(), "quotation_count": book.quotation_set.filter(**filters).count(), } - if hasattr(book, "suggestionlist"): + if hasattr(book, "suggestion_list"): data["suggested_books"] = get_list_suggestions( - book.suggestionlist, + book.suggestion_list, request.user, query=query, ignore_id=book.id, diff --git a/bookwyrm/views/list/lists.py b/bookwyrm/views/list/lists.py index 2514fad58b..90cca49737 100644 --- a/bookwyrm/views/list/lists.py +++ b/bookwyrm/views/list/lists.py @@ -21,6 +21,7 @@ def get(self, request): lists = ListsStream().get_list_stream(request.user) else: lists = models.List.objects.filter(privacy="public") + lists = lists.filter(suggests_for__isnull=True) paginated = Paginator(lists, 12) data = { "lists": paginated.get_page(request.GET.get("page")), @@ -53,7 +54,9 @@ class SavedLists(View): def get(self, request): """display book lists""" # hide lists with no approved books - lists = request.user.saved_lists.order_by("-updated_date") + lists = request.user.saved_lists.order_by("-updated_date").filter( + suggests_for__isnull=True + ) paginated = Paginator(lists, 12) data = { @@ -71,7 +74,11 @@ class UserLists(View): def get(self, request, username): """display a book list""" user = get_user_from_username(request.user, username) - lists = models.List.privacy_filter(request.user).filter(user=user) + lists = ( + models.List.privacy_filter(request.user) + .filter(user=user) + .filter(suggests_for__isnull=True) + ) paginated = Paginator(lists, 12) data = { From 630bbc1075a4740c5a7a7b5eab459ea1db3f1265 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 17:53:08 +0100 Subject: [PATCH 08/52] Update template with new column names --- bookwyrm/templates/book/suggestion_list/list.html | 6 +++--- bookwyrm/templates/book/suggestion_list/search.html | 2 +- bookwyrm/templates/lists/add_item_modal.html | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index cf97b53cde..a5249b3f89 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -4,8 +4,8 @@

{% trans "Suggestions" %}

-{% if book.suggestionlist %} -{% with book.suggestionlist.listitem_set.all as items %} +{% if book.suggestion_list %} +{% with book.suggestion_list.listitem_set.all as items %} {% if items|length == 0 %}
@@ -59,7 +59,7 @@

{% csrf_token %} - +

diff --git a/bookwyrm/templates/book/suggestion_list/search.html b/bookwyrm/templates/book/suggestion_list/search.html index 989583a0c6..8edd5fd7db 100644 --- a/bookwyrm/templates/book/suggestion_list/search.html +++ b/bookwyrm/templates/book/suggestion_list/search.html @@ -2,7 +2,7 @@ {% load utilities %} {% if request.user.is_authenticated %} -{% with book.suggestionlist as list %} +{% with book.suggestion_list as list %}

{% trans "Add suggestions" %}

diff --git a/bookwyrm/templates/lists/add_item_modal.html b/bookwyrm/templates/lists/add_item_modal.html index 184911cfca..254d17b99d 100644 --- a/bookwyrm/templates/lists/add_item_modal.html +++ b/bookwyrm/templates/lists/add_item_modal.html @@ -20,7 +20,7 @@ name="add-book-{{ book.id }}" method="POST" {% if is_suggestion %} - action="{% url 'book-add-suggestion' book_id=list.book.id %}{% if query %}?suggestion_query={{ query }}#suggestions-section{% endif %}" + action="{% url 'book-add-suggestion' book_id=list.suggests_for.id %}{% if query %}?suggestion_query={{ query }}#suggestions-section{% endif %}" {% else %} action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}" {% endif %} From 6de97aebde6e64afb70979ba51d391b10fbff43b Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 18:01:15 +0100 Subject: [PATCH 09/52] Add get_name for dynamically generated name and description --- bookwyrm/models/list.py | 18 ++++++++++++++++++ bookwyrm/templates/lists/embed-list.html | 2 +- bookwyrm/templates/lists/layout.html | 6 +++--- bookwyrm/templates/lists/list.html | 4 ++-- bookwyrm/templates/lists/list_items.html | 8 ++++---- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 2c5e1e9779..2b18b733c9 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -5,6 +5,7 @@ from django.db import models from django.db.models import Q from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from bookwyrm import activitypub from bookwyrm.settings import DOMAIN @@ -65,6 +66,23 @@ def collection_queryset(self): """list of books for this shelf, overrides OrderedCollectionMixin""" return self.books.filter(listitem__approved=True).order_by("listitem") + @property + def get_name(self): + if self.suggests_for: + return _("Suggestions for %(title)s") % {"title": self.suggests_for.title} + + return self.name + + @property + def get_description(self): + if self.suggests_for: + return _("This is the list of suggestions for %(title)s") % { + "title": self.suggests_for.title, + "url": self.suggests_for.local_path, + } + + return self.description + class Meta: """default sorting""" diff --git a/bookwyrm/templates/lists/embed-list.html b/bookwyrm/templates/lists/embed-list.html index d9a50a4646..411763eab2 100644 --- a/bookwyrm/templates/lists/embed-list.html +++ b/bookwyrm/templates/lists/embed-list.html @@ -21,7 +21,7 @@

- {% include 'snippets/trimmed_text.html' with full=list.description %} + {% include 'snippets/trimmed_text.html' with full=list.get_description %}
diff --git a/bookwyrm/templates/lists/layout.html b/bookwyrm/templates/lists/layout.html index e61d72b562..f1f52c7cab 100644 --- a/bookwyrm/templates/lists/layout.html +++ b/bookwyrm/templates/lists/layout.html @@ -1,12 +1,12 @@ {% extends 'layout.html' %} {% load i18n %} -{% block title %}{{ list.name }}{% endblock %} +{% block title %}{{ list.get_name }}{% endblock %} {% block content %}
-

{{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %}

+

{{ list.get_name }} {% include 'snippets/privacy-icons.html' with item=list %}

{% include 'lists/created_text.html' with list=list %}

@@ -28,7 +28,7 @@

{{ list.name }} {% include 'snippets/pr {% block breadcrumbs %}{% endblock %}
- {% include 'snippets/trimmed_text.html' with full=list.description %} + {% include 'snippets/trimmed_text.html' with full=list.get_description %}
diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index 6824f50076..04fcd253c8 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -12,7 +12,7 @@
  • {% trans "Lists" %}
  • - {{ list.name|truncatechars:30 }} + {{ list.get_name|truncatechars:30 }}
  • @@ -275,7 +275,7 @@

    data-copytext data-copytext-label="{% trans 'Copy embed code' %}" data-copytext-success="{% trans 'Copied!' %}" - ><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans trimmed with list_name=list.name site_name=site.name owner=list.user.display_name %} + ><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans trimmed with list_name=list.get_name site_name=site.name owner=list.user.display_name %} {{ list_name }}, a list by {{owner}} on {{ site_name }} {% endblocktrans %}" src="{{ embed_url }}"></iframe>

    diff --git a/bookwyrm/templates/lists/list_items.html b/bookwyrm/templates/lists/list_items.html index 1191a62647..1e73847a38 100644 --- a/bookwyrm/templates/lists/list_items.html +++ b/bookwyrm/templates/lists/list_items.html @@ -8,7 +8,7 @@

    - {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %} + {{ list.get_name }} {% include 'snippets/privacy-icons.html' with item=list %}

    {% if request.user.is_authenticated and request.user|saved:list %}
    @@ -33,9 +33,9 @@

    {% endwith %}
    -
    - {% if list.description %} - {{ list.description|to_markdown|safe|truncatechars_html:30 }} +
    + {% if list.get_description %} + {{ list.get_description|to_markdown|safe|truncatechars_html:30 }} {% else %}   {% endif %} From f81c1611fed4a60277741f99f5fb2df4475f9645 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 18:09:48 +0100 Subject: [PATCH 10/52] Limit what's displayed on list page --- bookwyrm/templates/lists/layout.html | 4 +++- bookwyrm/templates/lists/list.html | 2 ++ bookwyrm/views/list/list.py | 2 +- bookwyrm/views/list/lists.py | 4 +--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/lists/layout.html b/bookwyrm/templates/lists/layout.html index f1f52c7cab..fb4dacacb4 100644 --- a/bookwyrm/templates/lists/layout.html +++ b/bookwyrm/templates/lists/layout.html @@ -7,13 +7,15 @@

    {{ list.get_name }} {% include 'snippets/privacy-icons.html' with item=list %}

    + {% if list.suggests_for == None %}

    {% include 'lists/created_text.html' with list=list %}

    + {% endif %}
    - {% if request.user == list.user %} + {% if request.user == list.user and list.suggests_for == None %}
    {% trans "Edit List" as button_text %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_list" focus="edit_list_header" %} diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index 04fcd253c8..6a5208a555 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -177,6 +177,7 @@

    + {% if list.suggests_for == None %}

    {% trans "Sort List" %}

    @@ -199,6 +200,7 @@

    + {% endif %} {% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}

    {% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %} diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index 61f870928b..079d7a035e 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -73,7 +73,7 @@ def get(self, request, list_id, **kwargs): if request.user.is_authenticated: data["suggested_books"] = get_list_suggestions( - book_list, request.user, query=query + book_list, request.user, query=query, ignore_id=book_list.suggests_for.id ) return TemplateResponse(request, "lists/list.html", data) diff --git a/bookwyrm/views/list/lists.py b/bookwyrm/views/list/lists.py index 90cca49737..ff4c931779 100644 --- a/bookwyrm/views/list/lists.py +++ b/bookwyrm/views/list/lists.py @@ -54,9 +54,7 @@ class SavedLists(View): def get(self, request): """display book lists""" # hide lists with no approved books - lists = request.user.saved_lists.order_by("-updated_date").filter( - suggests_for__isnull=True - ) + lists = request.user.saved_lists.order_by("-updated_date") paginated = Paginator(lists, 12) data = { From 067ce297bc9c9ad11a4dce556263fd1c12c24002 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 18:14:04 +0100 Subject: [PATCH 11/52] black --- bookwyrm/models/list.py | 4 +++- bookwyrm/views/list/list.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 2b18b733c9..2862a0695c 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -76,7 +76,9 @@ def get_name(self): @property def get_description(self): if self.suggests_for: - return _("This is the list of suggestions for %(title)s") % { + return _( + "This is the list of suggestions for %(title)s" + ) % { "title": self.suggests_for.title, "url": self.suggests_for.local_path, } diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index 079d7a035e..ef9c7a53d3 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -73,7 +73,10 @@ def get(self, request, list_id, **kwargs): if request.user.is_authenticated: data["suggested_books"] = get_list_suggestions( - book_list, request.user, query=query, ignore_id=book_list.suggests_for.id + book_list, + request.user, + query=query, + ignore_id=book_list.suggests_for.id, ) return TemplateResponse(request, "lists/list.html", data) From 2faaea6ef76e19968a0ddf611f2b2cb472ac6027 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 18:14:26 +0100 Subject: [PATCH 12/52] docstrings --- bookwyrm/models/list.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 2862a0695c..68ce6e8621 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -68,6 +68,7 @@ def collection_queryset(self): @property def get_name(self): + """The name comes from the book title if it's a suggestion list""" if self.suggests_for: return _("Suggestions for %(title)s") % {"title": self.suggests_for.title} @@ -75,6 +76,7 @@ def get_name(self): @property def get_description(self): + """The description comes from the book title if it's a suggestion list""" if self.suggests_for: return _( "This is the list of suggestions for %(title)s" From 62c9c71343f187112d18188e7f82058d49576458 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 18:31:24 +0100 Subject: [PATCH 13/52] Replace ignore_id with ignore_book --- bookwyrm/views/books/books.py | 2 +- bookwyrm/views/list/list.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index b55af8ea6b..ff2d2ce551 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -132,7 +132,7 @@ def get(self, request, book_id, **kwargs): book.suggestion_list, request.user, query=query, - ignore_id=book.id, + ignore_book=book, ) return TemplateResponse(request, "book/book.html", data) diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index ef9c7a53d3..11b4cd77f7 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -76,7 +76,7 @@ def get(self, request, list_id, **kwargs): book_list, request.user, query=query, - ignore_id=book_list.suggests_for.id, + ignore_book=book_list.suggests_for, ) return TemplateResponse(request, "lists/list.html", data) @@ -97,7 +97,7 @@ def post(self, request, list_id): return redirect(book_list.local_path) -def get_list_suggestions(book_list, user, query=None, ignore_id=None): +def get_list_suggestions(book_list, user, query=None, ignore_book=None): """What books might a user want to add to a list""" if query: # search for books @@ -105,12 +105,12 @@ def get_list_suggestions(book_list, user, query=None, ignore_id=None): query, filters=[ ~Q(parent_work__editions__in=book_list.books.all()), - ~Q(parent_work__editions__in=[ignore_id]), + ~Q(parent_work__editions__in=[ignore_book]), ], ) # just suggest whatever books are nearby suggestions = user.shelfbook_set.filter(~Q(book__in=book_list.books.all())).exclude( - book__id=ignore_id + book=ignore_book ) suggestions = [s.book for s in suggestions[:5]] if len(suggestions) < 5: @@ -118,7 +118,7 @@ def get_list_suggestions(book_list, user, query=None, ignore_id=None): s.default_edition for s in models.Work.objects.filter( ~Q(editions__in=book_list.books.all()), - ~Q(editions__in=[ignore_id]), + ~Q(editions__in=[ignore_book]), ).order_by("-updated_date")[: 5 - len(suggestions)] ] return suggestions From 80ce4eca459d5bdb57e6767b9f7ec9b5e0973f71 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 18:46:34 +0100 Subject: [PATCH 14/52] Display the right lists in the Book sidebar --- bookwyrm/templates/book/book.html | 2 +- bookwyrm/views/books/books.py | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index e920a496ca..62df9b2ea6 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -367,7 +367,7 @@

    {% trans "Places" %}

    {% endif %} - {% if lists.exists or request.user.list_set.exists %} + {% if lists.exists or list_options.exists %}

    {% trans "Lists" %}

      diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index ff2d2ce551..8c4ee0e1c0 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -76,11 +76,15 @@ def get(self, request, book_id, **kwargs): queryset = queryset.select_related("user").order_by("-published_date") paginated = Paginator(queryset, PAGE_LENGTH) - query = request.GET.get("suggestion_query", "") - - lists = models.List.privacy_filter(request.user,).filter( - listitem__approved=True, - listitem__book__in=book.parent_work.editions.all(), + lists = ( + models.List.privacy_filter( + request.user, + ) + .filter( + listitem__approved=True, + listitem__book__in=book.parent_work.editions.all(), + ) + .filter(suggests_for__isnull=True) ) data = { "book": book, @@ -94,11 +98,13 @@ def get(self, request, book_id, **kwargs): "rating": reviews.aggregate(Avg("rating"))["rating__avg"], "lists": lists, "update_error": kwargs.get("update_error", False), - "query": query, + "query": request.GET.get("suggestion_query", ""), } if request.user.is_authenticated: - data["list_options"] = request.user.list_set.exclude(id__in=data["lists"]) + data["list_options"] = request.user.list_set.filter( + suggests_for__isnull=True + ).exclude(id__in=data["lists"]) data["file_link_form"] = forms.FileLinkForm() readthroughs = models.ReadThrough.objects.filter( user=request.user, @@ -131,7 +137,7 @@ def get(self, request, book_id, **kwargs): data["suggested_books"] = get_list_suggestions( book.suggestion_list, request.user, - query=query, + query=request.GET.get("suggestion_query", ""), ignore_book=book, ) From 88da8257d56ccf776d26de30a4cc5c642040f7c6 Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 19:44:02 +0100 Subject: [PATCH 15/52] Update ordered_collection.py --- bookwyrm/activitypub/ordered_collection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index 32e37c9966..7d8437d09b 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -40,6 +40,7 @@ class BookList(OrderedCollectionPrivate): summary: str = None curation: str = "closed" + book: str type: str = "BookList" From bee38cdf1f2594d46c763d42a66fab2463ea378c Mon Sep 17 00:00:00 2001 From: Joachim Date: Sun, 1 Jan 2023 19:44:33 +0100 Subject: [PATCH 16/52] Add defauult --- bookwyrm/activitypub/ordered_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index 7d8437d09b..a3da6d24cb 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -40,7 +40,7 @@ class BookList(OrderedCollectionPrivate): summary: str = None curation: str = "closed" - book: str + book: str = None type: str = "BookList" From 486278bbf44aaf4c63ffbd37852290ca9e71f0c9 Mon Sep 17 00:00:00 2001 From: Joachim Date: Tue, 1 Aug 2023 15:12:50 +0200 Subject: [PATCH 17/52] =?UTF-8?q?Black=20=F0=9F=95=B4=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bookwyrm/views/list/list.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index ceb5fc2185..227725b2eb 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -102,8 +102,8 @@ def post(self, request, list_id): def get_list_suggestions( - book_list, user, query=None, num_suggestions=5, ignore_book=None - ): + book_list, user, query=None, num_suggestions=5, ignore_book=None +): """What books might a user want to add to a list""" if query: # search for books @@ -115,9 +115,11 @@ def get_list_suggestions( ], ) # just suggest whatever books are nearby - suggestions = user.shelfbook_set.filter( - ~Q(book__in=book_list.books.all()) - ).exclude(book=ignore_book).distinct()[:num_suggestions] + suggestions = ( + user.shelfbook_set.filter(~Q(book__in=book_list.books.all())) + .exclude(book=ignore_book) + .distinct()[:num_suggestions] + ) suggestions = [s.book for s in suggestions[:num_suggestions]] if len(suggestions) < num_suggestions: others = [ From 0f93833b4fcb91d8b094591ae7ea7eeff654fa82 Mon Sep 17 00:00:00 2001 From: Joachim Date: Tue, 1 Aug 2023 15:12:57 +0200 Subject: [PATCH 18/52] Update migration --- .../{0173_list_suggests_for.py => 0180_list_suggests_for.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename bookwyrm/migrations/{0173_list_suggests_for.py => 0180_list_suggests_for.py} (83%) diff --git a/bookwyrm/migrations/0173_list_suggests_for.py b/bookwyrm/migrations/0180_list_suggests_for.py similarity index 83% rename from bookwyrm/migrations/0173_list_suggests_for.py rename to bookwyrm/migrations/0180_list_suggests_for.py index 48c9456967..dcd2e09858 100644 --- a/bookwyrm/migrations/0173_list_suggests_for.py +++ b/bookwyrm/migrations/0180_list_suggests_for.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.16 on 2023-01-01 16:19 +# Generated by Django 3.2.20 on 2023-08-01 13:12 import bookwyrm.models.fields from django.db import migrations @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ("bookwyrm", "0172_alter_user_preferred_language"), + ("bookwyrm", "0179_populate_sort_title"), ] operations = [ From 8dc8196a7b945870ac05c53b3ea87189f7d6af4a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 25 Aug 2024 18:20:14 -0700 Subject: [PATCH 19/52] Updates migration to avoid involved merge --- .../{0180_list_suggests_for.py => 0209_list_suggests_for.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename bookwyrm/migrations/{0180_list_suggests_for.py => 0209_list_suggests_for.py} (81%) diff --git a/bookwyrm/migrations/0180_list_suggests_for.py b/bookwyrm/migrations/0209_list_suggests_for.py similarity index 81% rename from bookwyrm/migrations/0180_list_suggests_for.py rename to bookwyrm/migrations/0209_list_suggests_for.py index dcd2e09858..fb36525a6a 100644 --- a/bookwyrm/migrations/0180_list_suggests_for.py +++ b/bookwyrm/migrations/0209_list_suggests_for.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.20 on 2023-08-01 13:12 +# Generated by Django 4.2.15 on 2024-08-26 01:12 import bookwyrm.models.fields from django.db import migrations @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ("bookwyrm", "0179_populate_sort_title"), + ("bookwyrm", "0208_merge_0207_merge_20240629_0626_0207_sqlparse_update"), ] operations = [ From 7603d188791c9565da7a08f0ace4647afd394386 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 25 Aug 2024 18:37:59 -0700 Subject: [PATCH 20/52] Wrap card columns in suggestions list --- bookwyrm/templates/book/suggestion_list/list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index a5249b3f89..d2b862d2cb 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -14,7 +14,7 @@

    {% else %} -
      +
        {% for item in items %}
      1. From cc70e836877724a17aa71a33726556ead957dee2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 25 Aug 2024 19:16:22 -0700 Subject: [PATCH 21/52] Re-styles suggstions to show the recommender's notes I also just shuffled the card around to fit more text. --- .../templates/book/suggestion_list/list.html | 37 +++++++++----- bookwyrm/templates/lists/list.html | 44 +---------------- bookwyrm/templates/lists/list_item_notes.html | 48 +++++++++++++++++++ 3 files changed, 73 insertions(+), 56 deletions(-) create mode 100644 bookwyrm/templates/lists/list_item_notes.html diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index d2b862d2cb..fd8eeb3682 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -1,4 +1,5 @@ {% load i18n %} +{% load book_display_tags %}

        {% trans "Suggestions" %} @@ -16,19 +17,29 @@

        {% else %}
          {% for item in items %} -
        1. +
        2. -
          - {% with book=item.book %} -
          - +
          + {% with item_book=item.book %} + +
          +

          + {% include 'snippets/book_titleby.html' with book=item_book %} +

          + {% if item_book|book_description %} +
          + {% with full=item_book|book_description trim_length=20 %} + {% include 'snippets/trimmed_text.html' with hide_more=True %} + {% endwith %} + {% endif %} + {% include "lists/list_item_notes.html" with list=book.suggestion_list hide_edit=True %}
          - -

          - {% include 'snippets/book_titleby.html' %} -

          {% endwith %}
        3. {% endfor %} -
        4. +
        5. {% include "book/suggestion_list/search.html" %}
        @@ -61,6 +72,6 @@

        - +

    {% endif %} diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index 804e71037f..3836720534 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -80,50 +80,8 @@ {% endwith %} + {% include "lists/list_item_notes.html" with item=item %} - {% if item.notes %} -
    - -
    -
    -
    - {% url 'user-feed' item.user|username as user_path %} - {% blocktrans trimmed with username=item.user.display_name %} - {{ username }} says: - {% endblocktrans %} -
    - {{ item.notes|to_markdown|safe }} -
    - {% if item.user == request.user %} -
    -
    - - - {% trans "Edit notes" %} - - - - {% include "lists/edit_item_form.html" with book=item.book %} -
    -
    - {% endif %} -
    -
    - {% elif item.user == request.user %} -
    -
    - - - {% trans "Add notes" %} - - - - {% include "lists/edit_item_form.html" with book=item.book %} -
    -
    - {% endif %}
    {% else %} + + {% blocktrans trimmed with count=book.suggestion_list.listitem_set.count|intcomma %} + View all {{ count }} suggestions + {% endblocktrans %} +
      {% for item in items %}
    1. {% include "book/suggestion_list/book_card.html" %}
    2. {% endfor %} -
    3. - {% include "book/suggestion_list/search.html" %} -
    + +
    + {% include "book/suggestion_list/search.html" %} +
    {% endif %} {% endwith %} {% else %} diff --git a/bookwyrm/templates/book/suggestion_list/search.html b/bookwyrm/templates/book/suggestion_list/search.html index 4446bbb346..583ac8727d 100644 --- a/bookwyrm/templates/book/suggestion_list/search.html +++ b/bookwyrm/templates/book/suggestion_list/search.html @@ -12,7 +12,7 @@ {% url 'book' book_id=book.id as partial_url %} {% with search_url=partial_url|add:"#add-suggestions" %} - {% include "lists/suggestion_search.html" with is_suggestion=True query_param="suggestion_query" %} + {% include "lists/suggestion_search.html" with is_suggestion=True query_param="suggestion_query" columns=True %} {% endwith %} {% endwith %} diff --git a/bookwyrm/templates/lists/list_item_notes.html b/bookwyrm/templates/lists/list_item_notes.html index c7fff17c43..dd03c8cc9a 100644 --- a/bookwyrm/templates/lists/list_item_notes.html +++ b/bookwyrm/templates/lists/list_item_notes.html @@ -2,20 +2,22 @@ {% load utilities %} {% if item.notes %} -
    - -
    -
    -
    - {% url 'user-feed' item.user|username as user_path %} - {% blocktrans trimmed with username=item.user.display_name %} - {{ username }} says: - {% endblocktrans %} -
    - {% include 'snippets/trimmed_text.html' with no_trim=no_trim full=item.notes %} -
    +
    +
    + +
    + {% url 'user-feed' item.user|username as user_path %} + {% blocktrans trimmed with username=item.user.display_name %} + {{ username }} says: + {% endblocktrans %} +
    +
    +
    + + {% include 'snippets/trimmed_text.html' with no_trim=no_trim full=item.notes %} + {% if item.user == request.user and not hide_edit %}
    diff --git a/bookwyrm/templates/lists/suggestion_search.html b/bookwyrm/templates/lists/suggestion_search.html index b432afa495..f8fa10cbd1 100644 --- a/bookwyrm/templates/lists/suggestion_search.html +++ b/bookwyrm/templates/lists/suggestion_search.html @@ -27,7 +27,9 @@ {% endif %} {% if suggested_books|length > 0 %} + {% for book in suggested_books %} + {% endif %} From 6b615d45b3fc237107521f76ecca2aa2cfe07e9b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 26 Aug 2024 13:57:22 -0700 Subject: [PATCH 27/52] Adds a couple suggestions tests --- bookwyrm/models/list.py | 5 +++ bookwyrm/tests/views/books/test_book.py | 50 ++++++++++++++++++++++++- bookwyrm/views/books/books.py | 6 +-- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 76fc91c6eb..a79123cc6d 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -162,6 +162,11 @@ def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs): self.embed_key = uuid.uuid4() update_fields = add_update_fields(update_fields, "embed_key") + # ensure that suggestion lists have the right properties + if self.suggests_for: + self.privacy = "public" + self.curation = "open" + super().save(*args, update_fields=update_fields, **kwargs) diff --git a/bookwyrm/tests/views/books/test_book.py b/bookwyrm/tests/views/books/test_book.py index ee6e7d8b47..d9048d5491 100644 --- a/bookwyrm/tests/views/books/test_book.py +++ b/bookwyrm/tests/views/books/test_book.py @@ -51,6 +51,11 @@ def setUpTestData(cls): remote_id="https://example.com/book/1", parent_work=cls.work, ) + cls.another_book = models.Edition.objects.create( + title="Another Example Edition", + remote_id="https://example.com/book/1", + parent_work=models.Work.objects.create(title="Another Work"), + ) models.SiteSettings.objects.create() @@ -267,7 +272,7 @@ def test_quotation_endposition(self, *_): """make sure the endposition is served as well""" view = views.Book.as_view() - _ = models.Quotation.objects.create( + models.Quotation.objects.create( user=self.local_user, book=self.book, content="hi", @@ -290,6 +295,49 @@ def test_quotation_endposition(self, *_): result.context_data["statuses"].object_list[0].endposition, "13" ) + def test_create_suggestion_list(self, *_): + """start a suggestion list for a book""" + self.assertFalse(hasattr(self.book, "suggestion_list")) + + view = views.create_suggestion_list + form = forms.SuggestionListForm() + form.data["user"] = self.local_user.id + form.data["suggests_for"] = self.book.id + request = self.factory.post("", form.data) + request.user = self.local_user + + view(request, self.book.id) + + self.book.refresh_from_db() + self.assertTrue(hasattr(self.book, "suggestion_list")) + + suggestion_list = self.book.suggestion_list + self.assertEqual(suggestion_list.suggests_for, self.book) + self.assertEqual(suggestion_list.privacy, "public") + self.assertEqual(suggestion_list.curation, "open") + + def test_book_add_suggestion(self, *_): + """Add a book to the recommendation list""" + suggestion_list = models.List.objects.create( + suggests_for=self.book, user=self.local_user + ) + view = views.book_add_suggestion + form = forms.ListItemForm() + form.data["user"] = self.local_user.id + form.data["book"] = self.another_book.id + form.data["book_list"] = suggestion_list.id + form.data["notes"] = "hello" + request = self.factory.post("", form.data) + request.user = self.local_user + + view(request, self.book.id) + + self.assertEqual(suggestion_list.listitem_set.count(), 1) + item = suggestion_list.listitem_set.first() + self.assertEqual(item.book, self.another_book) + self.assertEqual(item.user, self.local_user) + self.assertEqual(item.notes, "hello") + def _setup_cover_url(): """creates cover url mock""" diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 3ecf97f7fc..9c017538b9 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -249,11 +249,9 @@ def create_suggestion_list(request, book_id): if not form.is_valid(): return redirect("book", book.id) + # saving in two steps means django uses the model's custom save functionality, + # which adds an embed key and fixes the privacy and curation settings suggestion_list = form.save(request, commit=False) - - # default values for the suggestion list - suggestion_list.privacy = "public" - suggestion_list.curation = "open" suggestion_list.save() return redirect("book", book.id) From 003465398306a5f4a594c77ddd2034357b998569 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 26 Aug 2024 17:16:07 -0700 Subject: [PATCH 28/52] Uses a separate model for suggestion lists --- bookwyrm/forms/lists.py | 14 +- bookwyrm/models/__init__.py | 1 + bookwyrm/models/list.py | 185 +++++++++++------- .../templates/book/suggestion_list/list.html | 14 +- bookwyrm/templates/lists/embed-list.html | 2 +- bookwyrm/templates/lists/layout.html | 6 +- bookwyrm/templates/lists/list.html | 4 +- bookwyrm/templates/lists/list_items.html | 8 +- bookwyrm/urls.py | 8 +- bookwyrm/views/__init__.py | 13 +- bookwyrm/views/books/books.py | 64 +----- bookwyrm/views/list/list.py | 3 +- bookwyrm/views/list/lists.py | 8 +- 13 files changed, 162 insertions(+), 168 deletions(-) diff --git a/bookwyrm/forms/lists.py b/bookwyrm/forms/lists.py index 03979a45c4..d3bf5961e7 100644 --- a/bookwyrm/forms/lists.py +++ b/bookwyrm/forms/lists.py @@ -14,15 +14,21 @@ class Meta: fields = ["user", "name", "description", "curation", "privacy", "group"] +class ListItemForm(CustomForm): + class Meta: + model = models.ListItem + fields = ["user", "book", "book_list", "notes"] + + class SuggestionListForm(CustomForm): class Meta: - model = models.List - fields = ["user", "suggests_for"] + model = models.SuggestionList + fields = ["suggests_for"] -class ListItemForm(CustomForm): +class SuggestionListItemForm(CustomForm): class Meta: - model = models.ListItem + model = models.SuggestionListItem fields = ["user", "book", "book_list", "notes"] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 6bb99c7f25..e30004cc20 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -9,6 +9,7 @@ from .shelf import Shelf, ShelfBook from .list import List, ListItem +from .list import SuggestionList, SuggestionListItem from .status import Status, GeneratedNote, Comment, Quotation from .status import Review, ReviewRating diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index a79123cc6d..eb44814d95 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -23,76 +23,119 @@ ) -class List(OrderedCollectionMixin, BookWyrmModel): - """a list of books""" +class AbstractList(OrderedCollectionMixin, BookWyrmModel): + """Abstract model for regular lists and suggestion lists""" - name = fields.CharField(max_length=100) + embed_key = models.UUIDField(unique=True, null=True, editable=False) + activity_serializer = activitypub.BookList + privacy = fields.PrivacyField() user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="owner" ) - description = fields.TextField(blank=True, null=True, activitypub_field="summary") - privacy = fields.PrivacyField() - curation = fields.CharField( - max_length=255, default="closed", choices=CurationType.choices - ) - group = models.ForeignKey( - "Group", - on_delete=models.SET_NULL, - default=None, - blank=True, - null=True, - ) + + def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs): + """on save, update embed_key and avoid clash with existing code""" + if not self.embed_key: + self.embed_key = uuid.uuid4() + update_fields = add_update_fields(update_fields, "embed_key") + + super().save(*args, update_fields=update_fields, **kwargs) + + @property + def collection_queryset(self): + raise NotImplementedError + + class Meta: + """default sorting""" + + ordering = ("-updated_date",) + abstract = True + + +class SuggestionList(AbstractList): + """a list of user-provided suggested things to read next""" + books = models.ManyToManyField( "Edition", symmetrical=False, - through="ListItem", + through="SuggestionListItem", through_fields=("book_list", "book"), ) - embed_key = models.UUIDField(unique=True, null=True, editable=False) - activity_serializer = activitypub.BookList + suggests_for = fields.OneToOneField( "Edition", on_delete=models.PROTECT, activitypub_field="book", related_name="suggestion_list", - default=None, - null=True, + unique=True, ) - def get_remote_id(self): - """don't want the user to be in there in this case""" - return f"{BASE_URL}/list/{self.id}" - @property def collection_queryset(self): """list of books for this shelf, overrides OrderedCollectionMixin""" - return self.books.filter(listitem__approved=True).order_by("listitem") + return self.books.order_by("suggestionlistitem") + + def save(self, *args, **kwargs): + """on save, update embed_key and avoid clash with existing code""" + self.user = activitypub.get_representative() + self.privacy = "public" + + super().save(*args, **kwargs) + + def raise_not_editable(self, viewer): + """anyone can create a suggestion list, no one can edit""" + return + + def get_remote_id(self): + """don't want the user to be in there in this case""" + return f"{BASE_URL}/book/{self.suggests_for.id}/suggestions" @property - def get_name(self): + def name(self): """The name comes from the book title if it's a suggestion list""" - if self.suggests_for: - return _("Suggestions for %(title)s") % {"title": self.suggests_for.title} - - return self.name + return _("Suggestions for %(title)s") % {"title": self.suggests_for.title} @property - def get_description(self): + def description(self): """The description comes from the book title if it's a suggestion list""" - if self.suggests_for: - return _( - "This is the list of suggestions for %(title)s" - ) % { - "title": self.suggests_for.title, - "url": self.suggests_for.local_path, - } + return _( + "This is the list of suggestions for %(title)s" + ) % { + "title": self.suggests_for.title, + "url": self.suggests_for.local_path, + } - return self.description - class Meta: - """default sorting""" +class List(AbstractList): + """a list of books""" - ordering = ("-updated_date",) + books = models.ManyToManyField( + "Edition", + symmetrical=False, + through="ListItem", + through_fields=("book_list", "book"), + ) + name = fields.CharField(max_length=100) + description = fields.TextField(blank=True, null=True, activitypub_field="summary") + curation = fields.CharField( + max_length=255, default="closed", choices=CurationType.choices + ) + group = models.ForeignKey( + "Group", + on_delete=models.SET_NULL, + default=None, + blank=True, + null=True, + ) + + @property + def collection_queryset(self): + """list of books for this shelf, overrides OrderedCollectionMixin""" + return self.books.filter(listitem__approved=True).order_by("listitem") + + def get_remote_id(self): + """don't want the user to be in there in this case""" + return f"{BASE_URL}/list/{self.id}" def raise_not_editable(self, viewer): """the associated user OR the list owner can edit""" @@ -156,45 +199,22 @@ def remove_from_group(cls, owner, user): group=None, curation="closed" ) - def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs): - """on save, update embed_key and avoid clash with existing code""" - if not self.embed_key: - self.embed_key = uuid.uuid4() - update_fields = add_update_fields(update_fields, "embed_key") - - # ensure that suggestion lists have the right properties - if self.suggests_for: - self.privacy = "public" - self.curation = "open" - - super().save(*args, update_fields=update_fields, **kwargs) - -class ListItem(CollectionItemMixin, BookWyrmModel): - """ok""" +class AbstractListItem(CollectionItemMixin, BookWyrmModel): + """Abstracy class for list items for all types of lists""" book = fields.ForeignKey( "Edition", on_delete=models.PROTECT, activitypub_field="book" ) - book_list = models.ForeignKey("List", on_delete=models.CASCADE) user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="actor" ) notes = fields.HtmlField(blank=True, null=True, max_length=300) - approved = models.BooleanField(default=True) - order = fields.IntegerField() endorsement = models.ManyToManyField("User", related_name="endorsers") activity_serializer = activitypub.ListItem collection_field = "book_list" - def save(self, *args, **kwargs): - """Update the list's date""" - super().save(*args, **kwargs) - # tick the updated date on the parent list - self.book_list.updated_date = timezone.now() - self.book_list.save(broadcast=False, update_fields=["updated_date"]) - def raise_not_deletable(self, viewer): """the associated user OR the list owner can delete""" if self.book_list.user == viewer: @@ -211,5 +231,34 @@ class Meta: """A book may only be placed into a list once, and each order in the list may be used only once""" - unique_together = (("book", "book_list"), ("order", "book_list")) + unique_together = ("book", "book_list") ordering = ("-created_date",) + abstract = True + + +class ListItem(AbstractListItem): + """ok""" + + book_list = models.ForeignKey("List", on_delete=models.CASCADE) + approved = models.BooleanField(default=True) + order = fields.IntegerField() + + def save(self, *args, **kwargs): + """Update the list's date""" + super().save(*args, **kwargs) + # tick the updated date on the parent list + self.book_list.updated_date = timezone.now() + self.book_list.save(broadcast=False, update_fields=["updated_date"]) + + class Meta: + """A book may only be placed into a list once, + and each order in the list may be used only once""" + + unique_together = (("book", "book_list"), ("order", "book_list")) + + +class SuggestionListItem(AbstractListItem): + """items on a suggestion list""" + + book_list = models.ForeignKey("SuggestionList", on_delete=models.CASCADE) + endorsement = models.ManyToManyField("User", related_name="suggestion_endorsers") diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index 805fa43a69..09925377df 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -3,6 +3,11 @@

    {% trans "Suggestions" %} + + {% blocktrans trimmed with count=book.suggestion_list.suggestionlistitem_set.count|intcomma %} + View all {{ count }} suggestions + {% endblocktrans %} +

    {% if book.suggestion_list %} @@ -13,7 +18,7 @@

    {% endblocktrans %}

    -{% with book.suggestion_list.listitem_set.all|slice:3 as items %} +{% with book.suggestion_list.suggestionlistitem_set.all|slice:3 as items %} {% if items|length == 0 %}
    @@ -22,11 +27,6 @@

    {% else %} - - {% blocktrans trimmed with count=book.suggestion_list.listitem_set.count|intcomma %} - View all {{ count }} suggestions - {% endblocktrans %} -
      {% for item in items %}
    1. @@ -42,7 +42,7 @@

      {% endwith %} {% else %}
      -
      + {% csrf_token %} diff --git a/bookwyrm/templates/lists/embed-list.html b/bookwyrm/templates/lists/embed-list.html index 411763eab2..d9a50a4646 100644 --- a/bookwyrm/templates/lists/embed-list.html +++ b/bookwyrm/templates/lists/embed-list.html @@ -21,7 +21,7 @@

      - {% include 'snippets/trimmed_text.html' with full=list.get_description %} + {% include 'snippets/trimmed_text.html' with full=list.description %}
      diff --git a/bookwyrm/templates/lists/layout.html b/bookwyrm/templates/lists/layout.html index 9dd1c61dbd..c9003b3252 100644 --- a/bookwyrm/templates/lists/layout.html +++ b/bookwyrm/templates/lists/layout.html @@ -2,7 +2,7 @@ {% load i18n %} {% load list_page_tags %} -{% block title %}{{ list.get_name }}{% endblock %} +{% block title %}{{ list.name }}{% endblock %} {% block opengraph %} {% include 'snippets/opengraph.html' with title=list|opengraph_title description=list|opengraph_description %} @@ -11,7 +11,7 @@ {% block content %}
      -

      {{ list.get_name }} {% include 'snippets/privacy-icons.html' with item=list %}

      +

      {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %}

      {% if list.suggests_for == None %}

      {% include 'lists/created_text.html' with list=list %} @@ -35,7 +35,7 @@

      {{ list.get_name }} {% include 'snippet {% block breadcrumbs %}{% endblock %}
      - {% include 'snippets/trimmed_text.html' with full=list.get_description %} + {% include 'snippets/trimmed_text.html' with full=list.description %}
      diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index e76c9eec06..63f7ffcb28 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -12,7 +12,7 @@
    2. {% trans "Lists" %}
    3. - {{ list.get_name|truncatechars:30 }} + {{ list.name|truncatechars:30 }}
    4. @@ -182,7 +182,7 @@

      data-copytext data-copytext-label="{% trans 'Copy embed code' %}" data-copytext-success="{% trans 'Copied!' %}" - ><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans trimmed with list_name=list.get_name site_name=site.name owner=list.user.display_name %} + ><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans trimmed with list_name=list.name site_name=site.name owner=list.user.display_name %} {{ list_name }}, a list by {{owner}} on {{ site_name }} {% endblocktrans %}" src="{{ embed_url }}"></iframe>

      diff --git a/bookwyrm/templates/lists/list_items.html b/bookwyrm/templates/lists/list_items.html index 3e91e6f311..cff4c16c1c 100644 --- a/bookwyrm/templates/lists/list_items.html +++ b/bookwyrm/templates/lists/list_items.html @@ -8,7 +8,7 @@

      - {{ list.get_name }} {% include 'snippets/privacy-icons.html' with item=list %} + {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %}

      {% if request.user.is_authenticated and request.user|saved:list %}
      @@ -33,9 +33,9 @@

      {% endwith %}
      -
      - {% if list.get_description %} - {{ list.get_description|to_markdown|safe|truncatechars_html:30 }} +
      + {% if list.description %} + {{ list.description|to_markdown|safe|truncatechars_html:30 }} {% else %}   {% endif %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index cd071e8711..936327e6fa 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -761,12 +761,12 @@ name="book-update-remote", ), re_path( - rf"{BOOK_PATH}/create-suggestion-list/?$", - views.create_suggestion_list, - name="book-create-suggestion-list", + rf"{BOOK_PATH}/suggestions(.json)?/?$", + views.SuggestionList.as_view(), + name="suggestion-list", ), re_path( - rf"{BOOK_PATH}/book-add-suggestion/?$", + rf"{BOOK_PATH}/suggestions/add/?$", views.book_add_suggestion, name="book-add-suggestion", ), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index ddb07e5de9..7512115080 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -55,14 +55,7 @@ ) # books -from .books.books import ( - Book, - upload_cover, - add_description, - resolve_book, - create_suggestion_list, - book_add_suggestion, -) +from .books.books import Book, upload_cover, add_description, resolve_book from .books.series import BookSeriesBy from .books.books import update_book_from_remote from .books.edit_book import ( @@ -112,6 +105,10 @@ set_book_position, ) +# suggestion lists +from .suggestion_list import SuggestionList +from .suggestion_list import book_add_suggestion + # misc views from .author import Author, EditAuthor, update_author_from_remote from .directory import Directory diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 9c017538b9..5b42fba16d 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -4,8 +4,7 @@ from django.contrib.auth.decorators import login_required, permission_required from django.core.paginator import Paginator -from django.db import transaction -from django.db.models import Avg, Q, Max +from django.db.models import Avg, Q from django.http import Http404 from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse @@ -22,7 +21,7 @@ maybe_redirect_local_path, get_mergeable_object_or_404, ) -from bookwyrm.views.list.list import get_list_suggestions, increment_order_in_reverse +from bookwyrm.views.list.list import get_list_suggestions # pylint: disable=no-self-use @@ -85,15 +84,9 @@ def get(self, request, book_id, **kwargs): queryset = queryset.select_related("user").order_by("-published_date") paginated = Paginator(queryset, PAGE_LENGTH) - lists = ( - models.List.privacy_filter( - request.user, - ) - .filter( - listitem__approved=True, - listitem__book__in=book.parent_work.editions.all(), - ) - .filter(suggests_for__isnull=True) + lists = models.List.privacy_filter(request.user,).filter( + listitem__approved=True, + listitem__book__in=book.parent_work.editions.all(), ) data = { "book": book, @@ -113,9 +106,7 @@ def get(self, request, book_id, **kwargs): } if request.user.is_authenticated: - data["list_options"] = request.user.list_set.filter( - suggests_for__isnull=True - ).exclude(id__in=data["lists"]) + data["list_options"] = request.user.list_set.exclude(id__in=data["lists"]) data["file_link_form"] = forms.FileLinkForm() readthroughs = models.ReadThrough.objects.filter( user=request.user, @@ -238,46 +229,3 @@ def update_book_from_remote(request, book_id, connector_identifier): return Book().get(request, book_id, update_error=True) return redirect("book", book.id) - - -@login_required -@require_POST -def create_suggestion_list(request, book_id): - """create a suggestion_list""" - form = forms.SuggestionListForm(request.POST) - book = get_object_or_404(models.Edition, id=book_id) - - if not form.is_valid(): - return redirect("book", book.id) - # saving in two steps means django uses the model's custom save functionality, - # which adds an embed key and fixes the privacy and curation settings - suggestion_list = form.save(request, commit=False) - suggestion_list.save() - - return redirect("book", book.id) - - -@login_required -@require_POST -@transaction.atomic -def book_add_suggestion(request, book_id): - """put a book on the suggestion list""" - book_list = get_object_or_404(models.List, id=request.POST.get("book_list")) - - form = forms.ListItemForm(request.POST) - if not form.is_valid(): - return Book().get(request, book_id, add_failed=True) - - item = form.save(request, commit=False) - - # add the book at the latest order of approved books, before pending books - order_max = ( - book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[ - "order__max" - ] - ) or 0 - increment_order_in_reverse(book_list.id, order_max + 1) - item.order = order_max + 1 - item.save() - - return Book().get(request, book_id, add_succeeded=True) diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index 227725b2eb..ecac9f6b85 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -80,7 +80,6 @@ def get(self, request, list_id, **kwargs): book_list, request.user, query=query, - ignore_book=book_list.suggests_for, ) return TemplateResponse(request, "lists/list.html", data) @@ -92,7 +91,7 @@ def post(self, request, list_id): form = forms.ListForm(request.POST, instance=book_list) if not form.is_valid(): # this shouldn't happen - raise Exception(form.errors) + raise Exception(form.errors) # pylint: disable=broad-exception-raised book_list = form.save(request) if not book_list.curation == "group": book_list.group = None diff --git a/bookwyrm/views/list/lists.py b/bookwyrm/views/list/lists.py index 50f873ad8e..e35cd67df8 100644 --- a/bookwyrm/views/list/lists.py +++ b/bookwyrm/views/list/lists.py @@ -21,7 +21,6 @@ def get(self, request): lists = ListsStream().get_list_stream(request.user) else: lists = models.List.objects.filter(privacy="public") - lists = lists.filter(suggests_for__isnull=True) paginated = Paginator(lists, 12) data = { "lists": paginated.get_page(request.GET.get("page")), @@ -31,7 +30,6 @@ def get(self, request): return TemplateResponse(request, "lists/lists.html", data) @method_decorator(login_required, name="dispatch") - # pylint: disable=unused-argument def post(self, request): """create a book_list""" form = forms.ListForm(request.POST) @@ -72,11 +70,7 @@ class UserLists(View): def get(self, request, username): """display a book list""" user = get_user_from_username(request.user, username) - lists = ( - models.List.privacy_filter(request.user) - .filter(user=user) - .filter(suggests_for__isnull=True) - ) + lists = models.List.privacy_filter(request.user).filter(user=user) paginated = Paginator(lists, 12) data = { From 5879b4be5cb04693368aca8900b01d0bb6029e2b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 26 Aug 2024 17:54:55 -0700 Subject: [PATCH 29/52] Adds missing view file --- bookwyrm/views/suggestion_list.py | 99 +++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 bookwyrm/views/suggestion_list.py diff --git a/bookwyrm/views/suggestion_list.py b/bookwyrm/views/suggestion_list.py new file mode 100644 index 0000000000..3f2cb78f14 --- /dev/null +++ b/bookwyrm/views/suggestion_list.py @@ -0,0 +1,99 @@ +""" the good stuff! the books! """ +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.db import transaction +from django.shortcuts import get_object_or_404, redirect +from django.template.response import TemplateResponse +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.http import require_POST + +from bookwyrm import forms, models +from bookwyrm.activitypub import ActivitypubResponse +from bookwyrm.settings import PAGE_LENGTH +from bookwyrm.views import Book +from bookwyrm.views.helpers import is_api_request +from bookwyrm.views.list.list import get_list_suggestions + +# pylint: disable=no-self-use +class SuggestionList(View): + """book list page""" + + def get(self, request, book_id, **kwargs): + """display a book list""" + add_failed = kwargs.get("add_failed", False) + add_succeeded = kwargs.get("add_succeeded", False) + + book_list = get_object_or_404(models.SuggestionList, suggests_for=book_id) + + if is_api_request(request): + return ActivitypubResponse(book_list.to_activity(**request.GET)) + + items = book_list.suggestionlistitem_set.prefetch_related( + "user", "book", "book__authors" + ) + + paginated = Paginator(items, PAGE_LENGTH) + + page = paginated.get_page(request.GET.get("page")) + + embed_key = str(book_list.embed_key.hex) + embed_url = reverse("embed-list", args=[book_list.id, embed_key]) + embed_url = request.build_absolute_uri(embed_url) + + if request.GET: + embed_url = f"{embed_url}?{request.GET.urlencode()}" + + query = request.GET.get("q", "") + data = { + "list": book_list, + "items": page, + "page_range": paginated.get_elided_page_range( + page.number, on_each_side=2, on_ends=1 + ), + "query": query, + "embed_url": embed_url, + "add_failed": add_failed, + "add_succeeded": add_succeeded, + } + + if request.user.is_authenticated: + data["suggested_books"] = get_list_suggestions( + book_list, request.user, query=query, ignore_book=book_list.suggests_for + ) + return TemplateResponse(request, "lists/list.html", data) + + @method_decorator(login_required, name="dispatch") + def post(self, request, book_id): + """create a suggestion_list""" + form = forms.SuggestionListForm(request.POST) + book = get_object_or_404(models.Edition, id=book_id) + + if not form.is_valid(): + return redirect("book", book.id) + # saving in two steps means django uses the model's custom save functionality, + # which adds an embed key and fixes the privacy and curation settings + suggestion_list = form.save(request, commit=False) + suggestion_list.save() + + return redirect("book", book.id) + + +@login_required +@require_POST +@transaction.atomic +def book_add_suggestion(request, book_id): + """put a book on the suggestion list""" + _ = get_object_or_404( + models.SuggestionList, suggests_for=book_id, id=request.POST.get("book_list") + ) + + form = forms.SuggestionListItemForm(request.POST) + if not form.is_valid(): + return Book().get(request, book_id, add_failed=True) + + item = form.save(request, commit=False) + item.save() + + return Book().get(request, book_id, add_succeeded=True) From 2e15c227f36da4e75aa5c8e82d18764ad5ce5655 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 26 Aug 2024 17:57:21 -0700 Subject: [PATCH 30/52] Fixes adding books from list page view --- bookwyrm/templates/lists/add_item_modal.html | 6 +++--- bookwyrm/templates/lists/list.html | 5 ++--- bookwyrm/templates/lists/suggestion_search.html | 2 +- bookwyrm/views/list/list.py | 1 + bookwyrm/views/suggestion_list.py | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bookwyrm/templates/lists/add_item_modal.html b/bookwyrm/templates/lists/add_item_modal.html index 254d17b99d..322b69f999 100644 --- a/bookwyrm/templates/lists/add_item_modal.html +++ b/bookwyrm/templates/lists/add_item_modal.html @@ -4,7 +4,7 @@ {% load group_tags %} {% block modal-title %} -{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %} +{% if list.suggests_for or list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %} {% blocktrans trimmed with title=book|book_title %} Add "{{ title }}" to this list {% endblocktrans %} @@ -19,7 +19,7 @@
      diff --git a/bookwyrm/templates/book/suggestion_list/endorsement_button.html b/bookwyrm/templates/book/suggestion_list/endorsement_button.html new file mode 100644 index 0000000000..2ea8b6672f --- /dev/null +++ b/bookwyrm/templates/book/suggestion_list/endorsement_button.html @@ -0,0 +1,10 @@ +{% load i18n %} + + {% csrf_token %} + + diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index 09925377df..d7937b7264 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -18,8 +18,6 @@

      {% endblocktrans %}

      -{% with book.suggestion_list.suggestionlistitem_set.all|slice:3 as items %} - {% if items|length == 0 %}
      @@ -39,7 +37,6 @@

      {% include "book/suggestion_list/search.html" %}

      {% endif %} -{% endwith %} {% else %}
      diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index e6fbf12e77..b1f58c50f0 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -91,6 +91,11 @@ {% endblocktrans %}

      + {% if list.suggests_for %} + + {% endif %} {% if list.user == request.user or list.group|is_member:request.user %} \d+)/?$", + views.endorse_suggestion, + name="suggestion-endorse", + ), re_path( r"^author/(?P\d+)/update/(?P[\w\.]+)/?$", views.update_author_from_remote, diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 7aa86b6b54..2f36063bfb 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -107,7 +107,11 @@ # suggestion lists from .suggestion_list import SuggestionList -from .suggestion_list import book_add_suggestion, book_remove_suggestion +from .suggestion_list import ( + book_add_suggestion, + book_remove_suggestion, + endorse_suggestion, +) # misc views from .author import Author, EditAuthor, update_author_from_remote diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 5b42fba16d..96410a89bd 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -4,7 +4,7 @@ from django.contrib.auth.decorators import login_required, permission_required from django.core.paginator import Paginator -from django.db.models import Avg, Q +from django.db.models import Avg, Q, Count from django.http import Http404 from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse @@ -136,6 +136,15 @@ def get(self, request, book_id, **kwargs): "quotation_count": book.quotation_set.filter(**filters).count(), } if hasattr(book, "suggestion_list"): + + data["items"] = ( + book.suggestion_list.suggestionlistitem_set.prefetch_related( + "user", "book", "book__authors" + ) + .annotate(endorsement_count=Count("endorsement")) + .order_by("-endorsement_count")[:3] + ) + data["suggested_books"] = get_list_suggestions( book.suggestion_list, request.user, diff --git a/bookwyrm/views/suggestion_list.py b/bookwyrm/views/suggestion_list.py index 15ba5e132a..d0bef36477 100644 --- a/bookwyrm/views/suggestion_list.py +++ b/bookwyrm/views/suggestion_list.py @@ -2,6 +2,7 @@ from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db import transaction +from django.db.models import Count from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.urls import reverse @@ -30,8 +31,12 @@ def get(self, request, book_id, **kwargs): if is_api_request(request): return ActivitypubResponse(book_list.to_activity(**request.GET)) - items = book_list.suggestionlistitem_set.prefetch_related( - "user", "book", "book__authors" + items = ( + book_list.suggestionlistitem_set.prefetch_related( + "user", "book", "book__authors" + ) + .annotate(endorsement_count=Count("endorsement")) + .order_by("-endorsement_count") ) paginated = Paginator(items, PAGE_LENGTH) @@ -103,12 +108,30 @@ def book_add_suggestion(request, book_id): @require_POST @login_required -def book_remove_suggestion(request, _): +def book_remove_suggestion(request, book_id): """remove a book from a suggestion list""" - item = get_object_or_404(models.SuggestionListItem, id=request.POST.get("item")) + item = get_object_or_404( + models.SuggestionListItem, + id=request.POST.get("item"), + book_list__suggests_for=book_id, + ) item.raise_not_deletable(request.user) with transaction.atomic(): item.delete() return redirect_to_referer(request) + + +@require_POST +@login_required +def endorse_suggestion(request, book_id, item_id): + """endorse a suggestion""" + item = get_object_or_404( + models.SuggestionListItem, id=item_id, book_list__suggests_for=book_id + ) + if request.user not in item.endorsement.all(): + item.endorse(request.user) + else: + item.unendorse(request.user) + return redirect_to_referer(request) From 782ca090c7577e8efb08bf7ad6d47fadc25831fe Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 07:50:09 -0700 Subject: [PATCH 34/52] Updates tests --- bookwyrm/tests/views/books/test_book.py | 48 -------- bookwyrm/tests/views/test_suggestion_list.py | 109 +++++++++++++++++++ 2 files changed, 109 insertions(+), 48 deletions(-) create mode 100644 bookwyrm/tests/views/test_suggestion_list.py diff --git a/bookwyrm/tests/views/books/test_book.py b/bookwyrm/tests/views/books/test_book.py index d9048d5491..afd0fa47e0 100644 --- a/bookwyrm/tests/views/books/test_book.py +++ b/bookwyrm/tests/views/books/test_book.py @@ -51,11 +51,6 @@ def setUpTestData(cls): remote_id="https://example.com/book/1", parent_work=cls.work, ) - cls.another_book = models.Edition.objects.create( - title="Another Example Edition", - remote_id="https://example.com/book/1", - parent_work=models.Work.objects.create(title="Another Work"), - ) models.SiteSettings.objects.create() @@ -295,49 +290,6 @@ def test_quotation_endposition(self, *_): result.context_data["statuses"].object_list[0].endposition, "13" ) - def test_create_suggestion_list(self, *_): - """start a suggestion list for a book""" - self.assertFalse(hasattr(self.book, "suggestion_list")) - - view = views.create_suggestion_list - form = forms.SuggestionListForm() - form.data["user"] = self.local_user.id - form.data["suggests_for"] = self.book.id - request = self.factory.post("", form.data) - request.user = self.local_user - - view(request, self.book.id) - - self.book.refresh_from_db() - self.assertTrue(hasattr(self.book, "suggestion_list")) - - suggestion_list = self.book.suggestion_list - self.assertEqual(suggestion_list.suggests_for, self.book) - self.assertEqual(suggestion_list.privacy, "public") - self.assertEqual(suggestion_list.curation, "open") - - def test_book_add_suggestion(self, *_): - """Add a book to the recommendation list""" - suggestion_list = models.List.objects.create( - suggests_for=self.book, user=self.local_user - ) - view = views.book_add_suggestion - form = forms.ListItemForm() - form.data["user"] = self.local_user.id - form.data["book"] = self.another_book.id - form.data["book_list"] = suggestion_list.id - form.data["notes"] = "hello" - request = self.factory.post("", form.data) - request.user = self.local_user - - view(request, self.book.id) - - self.assertEqual(suggestion_list.listitem_set.count(), 1) - item = suggestion_list.listitem_set.first() - self.assertEqual(item.book, self.another_book) - self.assertEqual(item.user, self.local_user) - self.assertEqual(item.notes, "hello") - def _setup_cover_url(): """creates cover url mock""" diff --git a/bookwyrm/tests/views/test_suggestion_list.py b/bookwyrm/tests/views/test_suggestion_list.py new file mode 100644 index 0000000000..3eedfcc3a6 --- /dev/null +++ b/bookwyrm/tests/views/test_suggestion_list.py @@ -0,0 +1,109 @@ +""" test for app action functionality """ +from unittest.mock import patch + +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import forms, models, views +from bookwyrm.activitypub import ActivitypubResponse, get_representative +from bookwyrm.tests.validate_html import validate_html + + +class BookViews(TestCase): + """books books books""" + + @classmethod + def setUpTestData(cls): + """we need basic test data and mocks""" + with ( + patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), + patch("bookwyrm.activitystreams.populate_stream_task.delay"), + patch("bookwyrm.lists_stream.populate_lists_task.delay"), + ): + cls.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", + ) + cls.work = models.Work.objects.create(title="Test Work") + cls.book = models.Edition.objects.create( + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=cls.work, + ) + cls.another_book = models.Edition.objects.create( + title="Another Example Edition", + remote_id="https://example.com/book/1", + parent_work=models.Work.objects.create(title="Another Work"), + ) + + models.SiteSettings.objects.create() + + def setUp(self): + """individual test setup""" + self.factory = RequestFactory() + + def test_suggestion_list_get(self, *_): + """start a suggestion list for a book""" + models.SuggestionList.objects.create(suggests_for=self.book) + view = views.SuggestionList.as_view() + request = self.factory.get("") + request.user = self.local_user + + result = view(request, self.book.id) + validate_html(result.render()) + + def test_suggestion_list_get_json(self, *_): + """start a suggestion list for a book""" + models.SuggestionList.objects.create(suggests_for=self.book) + view = views.SuggestionList.as_view() + request = self.factory.get("") + request.user = self.local_user + + with patch("bookwyrm.views.suggestion_list.is_api_request") as is_api: + is_api.return_value = True + result = view(request, self.book.id) + self.assertIsInstance(result, ActivitypubResponse) + + def test_suggestion_create(self, *_): + """start a suggestion list for a book""" + self.assertFalse(hasattr(self.book, "suggestion_list")) + + view = views.SuggestionList.as_view() + form = forms.SuggestionListForm() + form.data["suggests_for"] = self.book.id + request = self.factory.post("", form.data) + request.user = self.local_user + + view(request, self.book.id) + + self.book.refresh_from_db() + self.assertTrue(hasattr(self.book, "suggestion_list")) + + suggestion_list = self.book.suggestion_list + self.assertEqual(suggestion_list.suggests_for, self.book) + self.assertEqual(suggestion_list.privacy, "public") + self.assertEqual(suggestion_list.user, get_representative()) + + def test_book_add_suggestion(self, *_): + """Add a book to the recommendation list""" + suggestion_list = models.SuggestionList.objects.create(suggests_for=self.book) + view = views.book_add_suggestion + form = forms.SuggestionListItemForm() + form.data["user"] = self.local_user.id + form.data["book"] = self.another_book.id + form.data["book_list"] = suggestion_list.id + form.data["notes"] = "hello" + request = self.factory.post("", form.data) + request.user = self.local_user + + view(request, self.book.id) + + self.assertEqual(suggestion_list.suggestionlistitem_set.count(), 1) + item = suggestion_list.suggestionlistitem_set.first() + self.assertEqual(item.book, self.another_book) + self.assertEqual(item.user, self.local_user) + self.assertEqual(item.notes, "hello") From 299ac0631da705c63130840ccf1be38ec0f3205e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 07:58:29 -0700 Subject: [PATCH 35/52] adds merge migration --- bookwyrm/migrations/0210_merge_20240827_1453.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 bookwyrm/migrations/0210_merge_20240827_1453.py diff --git a/bookwyrm/migrations/0210_merge_20240827_1453.py b/bookwyrm/migrations/0210_merge_20240827_1453.py new file mode 100644 index 0000000000..a54313a2be --- /dev/null +++ b/bookwyrm/migrations/0210_merge_20240827_1453.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.15 on 2024-08-27 14:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0209_suggestionlist_alter_listitem_options_and_more"), + ("bookwyrm", "0209_user_show_ratings"), + ] + + operations = [] From 5deb779efd8e3ade3cdec2b6d106690ecb9d8247 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 11:31:05 -0700 Subject: [PATCH 36/52] Associate suggestions with works instead of editions --- .../migrations/0210_merge_20240827_1453.py | 13 ------------ ...onlist_alter_listitem_options_and_more.py} | 6 +++--- bookwyrm/models/list.py | 2 +- bookwyrm/templates/book/book.html | 2 +- .../suggestion_list/endorsement_button.html | 2 +- .../templates/book/suggestion_list/list.html | 17 +++++++--------- .../book/suggestion_list/search.html | 5 +---- bookwyrm/templates/lists/add_item_modal.html | 10 +++++----- bookwyrm/templates/lists/list.html | 4 ++-- .../templates/lists/suggestion_search.html | 4 ++-- bookwyrm/views/books/books.py | 14 +++++++------ bookwyrm/views/list/list.py | 6 +++--- bookwyrm/views/suggestion_list.py | 20 ++++++++++++------- 13 files changed, 47 insertions(+), 58 deletions(-) delete mode 100644 bookwyrm/migrations/0210_merge_20240827_1453.py rename bookwyrm/migrations/{0209_suggestionlist_alter_listitem_options_and_more.py => 0210_suggestionlist_alter_listitem_options_and_more.py} (96%) diff --git a/bookwyrm/migrations/0210_merge_20240827_1453.py b/bookwyrm/migrations/0210_merge_20240827_1453.py deleted file mode 100644 index a54313a2be..0000000000 --- a/bookwyrm/migrations/0210_merge_20240827_1453.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 4.2.15 on 2024-08-27 14:53 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("bookwyrm", "0209_suggestionlist_alter_listitem_options_and_more"), - ("bookwyrm", "0209_user_show_ratings"), - ] - - operations = [] diff --git a/bookwyrm/migrations/0209_suggestionlist_alter_listitem_options_and_more.py b/bookwyrm/migrations/0210_suggestionlist_alter_listitem_options_and_more.py similarity index 96% rename from bookwyrm/migrations/0209_suggestionlist_alter_listitem_options_and_more.py rename to bookwyrm/migrations/0210_suggestionlist_alter_listitem_options_and_more.py index 85eb8d2fb9..1864701dec 100644 --- a/bookwyrm/migrations/0209_suggestionlist_alter_listitem_options_and_more.py +++ b/bookwyrm/migrations/0210_suggestionlist_alter_listitem_options_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.15 on 2024-08-27 01:20 +# Generated by Django 4.2.15 on 2024-08-27 17:27 import bookwyrm.models.activitypub_mixin import bookwyrm.models.fields @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ - ("bookwyrm", "0208_merge_0207_merge_20240629_0626_0207_sqlparse_update"), + ("bookwyrm", "0209_user_show_ratings"), ] operations = [ @@ -140,7 +140,7 @@ class Migration(migrations.Migration): field=bookwyrm.models.fields.OneToOneField( on_delete=django.db.models.deletion.PROTECT, related_name="suggestion_list", - to="bookwyrm.edition", + to="bookwyrm.work", ), ), migrations.AddField( diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 23e044f58f..3301983cb3 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -63,7 +63,7 @@ class SuggestionList(AbstractList): ) suggests_for = fields.OneToOneField( - "Edition", + "Work", on_delete=models.PROTECT, activitypub_field="book", related_name="suggestion_list", diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 49474147fe..0fe8e13fdc 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -423,7 +423,7 @@

      {% trans "Lists" %}

      - {% include "book/suggestion_list/list.html" %} + {% include "book/suggestion_list/list.html" with list=suggstion_list %}
      {% endwith %} {% endblock %} diff --git a/bookwyrm/templates/book/suggestion_list/endorsement_button.html b/bookwyrm/templates/book/suggestion_list/endorsement_button.html index 2ea8b6672f..9ddad536cd 100644 --- a/bookwyrm/templates/book/suggestion_list/endorsement_button.html +++ b/bookwyrm/templates/book/suggestion_list/endorsement_button.html @@ -1,5 +1,5 @@ {% load i18n %} - + {% csrf_token %}

      diff --git a/bookwyrm/templates/book/suggestion_list/search.html b/bookwyrm/templates/book/suggestion_list/search.html index 583ac8727d..9a5b9cb2ab 100644 --- a/bookwyrm/templates/book/suggestion_list/search.html +++ b/bookwyrm/templates/book/suggestion_list/search.html @@ -2,7 +2,6 @@ {% load utilities %} {% if request.user.is_authenticated %} -{% with book.suggestion_list as list %}
      @@ -10,11 +9,9 @@ - {% url 'book' book_id=book.id as partial_url %} - {% with search_url=partial_url|add:"#add-suggestions" %} + {% with search_url=request.path|add:"#add-suggestions" %} {% include "lists/suggestion_search.html" with is_suggestion=True query_param="suggestion_query" columns=True %} {% endwith %}
      -{% endwith %} {% endif %} diff --git a/bookwyrm/templates/lists/add_item_modal.html b/bookwyrm/templates/lists/add_item_modal.html index 322b69f999..96336809e0 100644 --- a/bookwyrm/templates/lists/add_item_modal.html +++ b/bookwyrm/templates/lists/add_item_modal.html @@ -4,7 +4,7 @@ {% load group_tags %} {% block modal-title %} -{% if list.suggests_for or list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %} +{% if is_suggestion or list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %} {% blocktrans trimmed with title=book|book_title %} Add "{{ title }}" to this list {% endblocktrans %} @@ -19,10 +19,10 @@
      {% endblock %} @@ -39,7 +39,7 @@
      {% if list.suggests_for %} {% endif %} {% if list.user == request.user or list.group|is_member:request.user %} @@ -172,7 +172,7 @@

      {% trans "Suggest Books" %} {% endif %}

      - {% include "lists/suggestion_search.html" with query_param="q" search_url=add_book_url %} + {% include "lists/suggestion_search.html" with query_param="q" search_url=request.path %} {% endif %}

      diff --git a/bookwyrm/templates/lists/suggestion_search.html b/bookwyrm/templates/lists/suggestion_search.html index a522e4e079..b852d8b175 100644 --- a/bookwyrm/templates/lists/suggestion_search.html +++ b/bookwyrm/templates/lists/suggestion_search.html @@ -48,13 +48,13 @@ class="button is-small is-link" data-modal-open="{{ modal_id }}" > - {% if list.suggests_for or list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %} + {% if is_suggestion or list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %} {% trans "Add" %} {% else %} {% trans "Suggest" %} {% endif %} - {% include "lists/add_item_modal.html" with id=modal_id is_suggestion=is_suggestion %} + {% include "lists/add_item_modal.html" with id=modal_id is_suggestion=is_suggestion list=list %}

    diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 96410a89bd..2f4a599429 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -90,6 +90,7 @@ def get(self, request, book_id, **kwargs): ) data = { "book": book, + "work": book.parent_work, "statuses": paginated.get_page(request.GET.get("page")), "review_count": reviews.count(), "ratings": ( @@ -135,21 +136,22 @@ def get(self, request, book_id, **kwargs): "comment_count": book.comment_set.filter(**filters).count(), "quotation_count": book.quotation_set.filter(**filters).count(), } - if hasattr(book, "suggestion_list"): - + if hasattr(book.parent_work, "suggestion_list"): + suggestion_list = book.parent_work.suggestion_list + data["suggestion_list"] = suggestion_list data["items"] = ( - book.suggestion_list.suggestionlistitem_set.prefetch_related( - "user", "book", "book__authors" + suggestion_list.suggestionlistitem_set.prefetch_related( + "user", "book", "book__authors", "endorsement" ) .annotate(endorsement_count=Count("endorsement")) .order_by("-endorsement_count")[:3] ) data["suggested_books"] = get_list_suggestions( - book.suggestion_list, + suggestion_list, request.user, query=request.GET.get("suggestion_query", ""), - ignore_book=book, + ignore_book=book.parent_work, ) return TemplateResponse(request, "book/book.html", data) diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index 2ad9703902..87acb43258 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -112,13 +112,13 @@ def get_list_suggestions( query, filters=[ ~Q(parent_work__editions__in=book_list.books.all()), - ~Q(parent_work__editions__in=[ignore_book]), + ~Q(parent_work=ignore_book), ], ) # just suggest whatever books are nearby suggestions = ( user.shelfbook_set.filter(~Q(book__in=book_list.books.all())) - .exclude(book=ignore_book) + .exclude(book__parent_work=ignore_book) .distinct()[:num_suggestions] ) suggestions = [s.book for s in suggestions[:num_suggestions]] @@ -127,7 +127,7 @@ def get_list_suggestions( s.default_edition for s in models.Work.objects.filter( ~Q(editions__in=book_list.books.all()), - ~Q(editions__in=[ignore_book]), + ~Q(id=ignore_book.id), ) .distinct() .order_by("-updated_date")[:num_suggestions] diff --git a/bookwyrm/views/suggestion_list.py b/bookwyrm/views/suggestion_list.py index d0bef36477..e6187ac6fe 100644 --- a/bookwyrm/views/suggestion_list.py +++ b/bookwyrm/views/suggestion_list.py @@ -2,8 +2,8 @@ from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db import transaction -from django.db.models import Count -from django.shortcuts import get_object_or_404, redirect +from django.db.models import Count, Q +from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.urls import reverse from django.utils.decorators import method_decorator @@ -26,7 +26,13 @@ def get(self, request, book_id, **kwargs): add_failed = kwargs.get("add_failed", False) add_succeeded = kwargs.get("add_succeeded", False) - book_list = get_object_or_404(models.SuggestionList, suggests_for=book_id) + work = models.Work.objects.filter( + Q(id=book_id) | Q(editions=book_id) + ).distinct() + print(work.count()) + work = work.first() + + book_list = get_object_or_404(models.SuggestionList, suggests_for=work) if is_api_request(request): return ActivitypubResponse(book_list.to_activity(**request.GET)) @@ -53,6 +59,7 @@ def get(self, request, book_id, **kwargs): query = request.GET.get("q", "") data = { "list": book_list, + "work": book_list.suggests_for, "items": page, "page_range": paginated.get_elided_page_range( page.number, on_each_side=2, on_ends=1 @@ -72,19 +79,18 @@ def get(self, request, book_id, **kwargs): return TemplateResponse(request, "lists/list.html", data) @method_decorator(login_required, name="dispatch") - def post(self, request, book_id): + def post(self, request, book_id): # pylint: disable=unused-argument """create a suggestion_list""" form = forms.SuggestionListForm(request.POST) - book = get_object_or_404(models.Edition, id=book_id) if not form.is_valid(): - return redirect("book", book.id) + return redirect_to_referer(request) # saving in two steps means django uses the model's custom save functionality, # which adds an embed key and fixes the privacy and curation settings suggestion_list = form.save(request, commit=False) suggestion_list.save() - return redirect("book", book.id) + return redirect_to_referer(request) @login_required From 1bccffaa9bda7b70de75394ba5281a5033c24ac9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 11:34:35 -0700 Subject: [PATCH 37/52] Uses separate activitypub type of suggestion lists --- bookwyrm/activitypub/__init__.py | 2 +- bookwyrm/activitypub/ordered_collection.py | 10 +++++++++- bookwyrm/models/list.py | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 41decd68af..0705dc2410 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -15,7 +15,7 @@ from .note import Tombstone from .ordered_collection import OrderedCollection, OrderedCollectionPage from .ordered_collection import CollectionItem, ListItem, ShelfItem -from .ordered_collection import BookList, Shelf +from .ordered_collection import BookList, SuggestionList, Shelf from .person import Person, PublicKey from .response import ActivitypubResponse from .book import Edition, Work, Author diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index 7d811331cc..a88d373287 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -39,10 +39,18 @@ class BookList(OrderedCollectionPrivate): summary: str = None curation: str = "closed" - book: str = None type: str = "BookList" +@dataclass(init=False) +class SuggestionList(OrderedCollectionPrivate): + """structure of an ordered collection activity""" + + summary: str = None + book: str = None + type: str = "SuggestionList" + + # pylint: disable=invalid-name @dataclass(init=False) class OrderedCollectionPage(ActivityObject): diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 3301983cb3..1becf8f080 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -69,6 +69,7 @@ class SuggestionList(AbstractList): related_name="suggestion_list", unique=True, ) + activity_serializer = activitypub.SuggestionList @property def collection_queryset(self): From aee089fcca20529ae3c7e4fd5c74a3acb50142ab Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 11:43:27 -0700 Subject: [PATCH 38/52] Support suggestion filtering when ignore book isn't present --- bookwyrm/views/list/list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/list/list.py b/bookwyrm/views/list/list.py index 87acb43258..9f333ceaa4 100644 --- a/bookwyrm/views/list/list.py +++ b/bookwyrm/views/list/list.py @@ -127,7 +127,7 @@ def get_list_suggestions( s.default_edition for s in models.Work.objects.filter( ~Q(editions__in=book_list.books.all()), - ~Q(id=ignore_book.id), + ~Q(id=ignore_book.id if ignore_book else None), ) .distinct() .order_by("-updated_date")[:num_suggestions] From 6b622bac3ee1761cb3488aa163879181bbb757c3 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 13:49:15 -0700 Subject: [PATCH 39/52] Adds more tests --- bookwyrm/tests/views/books/test_book.py | 42 +++++++++ bookwyrm/tests/views/test_suggestion_list.py | 95 +++++++++++++++++--- bookwyrm/views/books/books.py | 8 +- bookwyrm/views/suggestion_list.py | 4 +- 4 files changed, 132 insertions(+), 17 deletions(-) diff --git a/bookwyrm/tests/views/books/test_book.py b/bookwyrm/tests/views/books/test_book.py index afd0fa47e0..49f2815dd1 100644 --- a/bookwyrm/tests/views/books/test_book.py +++ b/bookwyrm/tests/views/books/test_book.py @@ -51,6 +51,11 @@ def setUpTestData(cls): remote_id="https://example.com/book/1", parent_work=cls.work, ) + cls.another_book = models.Edition.objects.create( + title="Another Example Edition", + remote_id="https://example.com/book/1", + parent_work=models.Work.objects.create(title="Another Work"), + ) models.SiteSettings.objects.create() @@ -134,6 +139,43 @@ def test_book_page_statuses(self, *_): self.assertEqual(result.status_code, 200) self.assertEqual(result.context_data["statuses"].object_list[0], quote) + @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") + def test_book_page_suggestions(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Book.as_view() + suggestion_list = models.SuggestionList.objects.create(suggests_for=self.work) + + request = self.factory.get("") + request.user = self.local_user + with patch("bookwyrm.views.books.books.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.book.id, user_statuses="review") + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + + self.assertEqual(result.status_code, 200) + self.assertEqual(result.context_data["suggestion_list"], suggestion_list) + + @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") + def test_book_page_suggestions_with_items(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Book.as_view() + suggestion_list = models.SuggestionList.objects.create(suggests_for=self.work) + models.SuggestionListItem.objects.create( + book_list=suggestion_list, user=self.local_user, book=self.another_book + ) + + request = self.factory.get("") + request.user = self.local_user + with patch("bookwyrm.views.books.books.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.book.id, user_statuses="review") + self.assertIsInstance(result, TemplateResponse) + validate_html(result.render()) + + self.assertEqual(result.status_code, 200) + self.assertEqual(result.context_data["suggestion_list"], suggestion_list) + def test_book_page_invalid_id(self): """there are so many views, this just makes sure it LOADS""" view = views.Book.as_view() diff --git a/bookwyrm/tests/views/test_suggestion_list.py b/bookwyrm/tests/views/test_suggestion_list.py index 3eedfcc3a6..5cdb9fad8a 100644 --- a/bookwyrm/tests/views/test_suggestion_list.py +++ b/bookwyrm/tests/views/test_suggestion_list.py @@ -1,6 +1,7 @@ """ test for app action functionality """ from unittest.mock import patch +from django.core.exceptions import PermissionDenied from django.test import TestCase from django.test.client import RequestFactory @@ -28,6 +29,14 @@ def setUpTestData(cls): localname="mouse", remote_id="https://example.com/users/mouse", ) + cls.another_user = models.User.objects.create_user( + "rat@local.com", + "rat@rat.com", + "ratword", + local=True, + localname="rat", + remote_id="https://example.com/users/rat", + ) cls.work = models.Work.objects.create(title="Test Work") cls.book = models.Edition.objects.create( title="Example Edition", @@ -48,7 +57,7 @@ def setUp(self): def test_suggestion_list_get(self, *_): """start a suggestion list for a book""" - models.SuggestionList.objects.create(suggests_for=self.book) + models.SuggestionList.objects.create(suggests_for=self.work) view = views.SuggestionList.as_view() request = self.factory.get("") request.user = self.local_user @@ -58,7 +67,7 @@ def test_suggestion_list_get(self, *_): def test_suggestion_list_get_json(self, *_): """start a suggestion list for a book""" - models.SuggestionList.objects.create(suggests_for=self.book) + models.SuggestionList.objects.create(suggests_for=self.work) view = views.SuggestionList.as_view() request = self.factory.get("") request.user = self.local_user @@ -70,40 +79,106 @@ def test_suggestion_list_get_json(self, *_): def test_suggestion_create(self, *_): """start a suggestion list for a book""" - self.assertFalse(hasattr(self.book, "suggestion_list")) + self.assertFalse(hasattr(self.work, "suggestion_list")) view = views.SuggestionList.as_view() form = forms.SuggestionListForm() - form.data["suggests_for"] = self.book.id + form.data["suggests_for"] = self.work.id request = self.factory.post("", form.data) request.user = self.local_user view(request, self.book.id) - self.book.refresh_from_db() - self.assertTrue(hasattr(self.book, "suggestion_list")) + self.work.refresh_from_db() + self.assertTrue(hasattr(self.work, "suggestion_list")) - suggestion_list = self.book.suggestion_list - self.assertEqual(suggestion_list.suggests_for, self.book) + suggestion_list = self.work.suggestion_list + self.assertEqual(suggestion_list.suggests_for, self.work) self.assertEqual(suggestion_list.privacy, "public") self.assertEqual(suggestion_list.user, get_representative()) def test_book_add_suggestion(self, *_): """Add a book to the recommendation list""" - suggestion_list = models.SuggestionList.objects.create(suggests_for=self.book) + suggestion_list = models.SuggestionList.objects.create(suggests_for=self.work) view = views.book_add_suggestion + form = forms.SuggestionListItemForm() form.data["user"] = self.local_user.id form.data["book"] = self.another_book.id form.data["book_list"] = suggestion_list.id form.data["notes"] = "hello" + request = self.factory.post("", form.data) request.user = self.local_user - view(request, self.book.id) + view(request, self.work.id) self.assertEqual(suggestion_list.suggestionlistitem_set.count(), 1) item = suggestion_list.suggestionlistitem_set.first() self.assertEqual(item.book, self.another_book) self.assertEqual(item.user, self.local_user) self.assertEqual(item.notes, "hello") + + def test_book_remove_suggestion(self, *_): + """Remove a book from the recommendation list""" + suggestion_list = models.SuggestionList.objects.create(suggests_for=self.work) + item = models.SuggestionListItem.objects.create( + book_list=suggestion_list, user=self.local_user, book=self.another_book + ) + self.assertEqual(suggestion_list.suggestionlistitem_set.count(), 1) + + view = views.book_remove_suggestion + request = self.factory.post("", {"item": item.id}) + request.user = self.local_user + + view(request, self.work.id) + + self.assertEqual(suggestion_list.suggestionlistitem_set.count(), 0) + + def test_book_remove_suggestion_without_permission(self, *_): + """Remove a book from the recommendation list""" + suggestion_list = models.SuggestionList.objects.create(suggests_for=self.work) + item = models.SuggestionListItem.objects.create( + book_list=suggestion_list, user=self.local_user, book=self.another_book + ) + self.assertEqual(suggestion_list.suggestionlistitem_set.count(), 1) + + view = views.book_remove_suggestion + request = self.factory.post("", {"item": item.id}) + request.user = self.another_user + + with self.assertRaises(PermissionDenied): + view(request, self.work.id) + + self.assertEqual(suggestion_list.suggestionlistitem_set.count(), 1) + + def test_endorse_suggestion(self, *_): + """Endorse a suggestion""" + suggestion_list = models.SuggestionList.objects.create(suggests_for=self.work) + item = models.SuggestionListItem.objects.create( + book_list=suggestion_list, user=self.local_user, book=self.another_book + ) + self.assertEqual(item.endorsement.count(), 0) + view = views.endorse_suggestion + request = self.factory.post("") + request.user = self.another_user + + view(request, self.work.id, item.id) + + self.assertEqual(item.endorsement.count(), 1) + + def test_endorse_suggestion_by_self(self, *_): + """Endorse a suggestion error handling""" + suggestion_list = models.SuggestionList.objects.create(suggests_for=self.work) + item = models.SuggestionListItem.objects.create( + book_list=suggestion_list, user=self.local_user, book=self.another_book + ) + self.assertEqual(item.endorsement.count(), 0) + view = views.endorse_suggestion + request = self.factory.post("") + request.user = self.local_user + + view(request, self.work.id, item.id) + + # no impact + self.assertEqual(item.endorsement.count(), 0) diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 2f4a599429..ad7f62e88d 100644 --- a/bookwyrm/views/books/books.py +++ b/bookwyrm/views/books/books.py @@ -137,10 +137,10 @@ def get(self, request, book_id, **kwargs): "quotation_count": book.quotation_set.filter(**filters).count(), } if hasattr(book.parent_work, "suggestion_list"): - suggestion_list = book.parent_work.suggestion_list - data["suggestion_list"] = suggestion_list + data["suggestion_list"] = book.parent_work.suggestion_list data["items"] = ( - suggestion_list.suggestionlistitem_set.prefetch_related( + data["suggestion_list"] + .suggestionlistitem_set.prefetch_related( "user", "book", "book__authors", "endorsement" ) .annotate(endorsement_count=Count("endorsement")) @@ -148,7 +148,7 @@ def get(self, request, book_id, **kwargs): ) data["suggested_books"] = get_list_suggestions( - suggestion_list, + data["suggestion_list"], request.user, query=request.GET.get("suggestion_query", ""), ignore_book=book.parent_work, diff --git a/bookwyrm/views/suggestion_list.py b/bookwyrm/views/suggestion_list.py index e6187ac6fe..a2cc9403ec 100644 --- a/bookwyrm/views/suggestion_list.py +++ b/bookwyrm/views/suggestion_list.py @@ -95,7 +95,6 @@ def post(self, request, book_id): # pylint: disable=unused-argument @login_required @require_POST -@transaction.atomic def book_add_suggestion(request, book_id): """put a book on the suggestion list""" _ = get_object_or_404( @@ -106,8 +105,7 @@ def book_add_suggestion(request, book_id): if not form.is_valid(): return Book().get(request, book_id, add_failed=True) - item = form.save(request, commit=False) - item.save() + form.save(request) return redirect_to_referer(request) From 0b171b0ba9f94dddab11d491abd2800bbf122ea5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 18:22:50 -0700 Subject: [PATCH 40/52] Adds type annotation to suggestion list view file --- bookwyrm/views/suggestion_list.py | 20 +++++++++++++------- mypy.ini | 6 ++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/bookwyrm/views/suggestion_list.py b/bookwyrm/views/suggestion_list.py index a2cc9403ec..f8b6cb7558 100644 --- a/bookwyrm/views/suggestion_list.py +++ b/bookwyrm/views/suggestion_list.py @@ -1,8 +1,11 @@ """ the good stuff! the books! """ +from typing import Any + from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db import transaction from django.db.models import Count, Q +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.urls import reverse @@ -21,7 +24,9 @@ class SuggestionList(View): """book list page""" - def get(self, request, book_id, **kwargs): + def get( + self, request: HttpRequest, book_id: int, **kwargs: Any + ) -> ActivitypubResponse | TemplateResponse: """display a book list""" add_failed = kwargs.get("add_failed", False) add_succeeded = kwargs.get("add_succeeded", False) @@ -29,7 +34,6 @@ def get(self, request, book_id, **kwargs): work = models.Work.objects.filter( Q(id=book_id) | Q(editions=book_id) ).distinct() - print(work.count()) work = work.first() book_list = get_object_or_404(models.SuggestionList, suggests_for=work) @@ -49,7 +53,7 @@ def get(self, request, book_id, **kwargs): page = paginated.get_page(request.GET.get("page")) - embed_key = str(book_list.embed_key.hex) + embed_key = str(book_list.embed_key.hex) # type: ignore embed_url = reverse("embed-list", args=[book_list.id, embed_key]) embed_url = request.build_absolute_uri(embed_url) @@ -79,7 +83,9 @@ def get(self, request, book_id, **kwargs): return TemplateResponse(request, "lists/list.html", data) @method_decorator(login_required, name="dispatch") - def post(self, request, book_id): # pylint: disable=unused-argument + def post( + self, request: HttpRequest, book_id: int + ) -> Any: # pylint: disable=unused-argument """create a suggestion_list""" form = forms.SuggestionListForm(request.POST) @@ -95,7 +101,7 @@ def post(self, request, book_id): # pylint: disable=unused-argument @login_required @require_POST -def book_add_suggestion(request, book_id): +def book_add_suggestion(request: HttpRequest, book_id: int) -> Any: """put a book on the suggestion list""" _ = get_object_or_404( models.SuggestionList, suggests_for=book_id, id=request.POST.get("book_list") @@ -112,7 +118,7 @@ def book_add_suggestion(request, book_id): @require_POST @login_required -def book_remove_suggestion(request, book_id): +def book_remove_suggestion(request: HttpRequest, book_id: int) -> Any: """remove a book from a suggestion list""" item = get_object_or_404( models.SuggestionListItem, @@ -129,7 +135,7 @@ def book_remove_suggestion(request, book_id): @require_POST @login_required -def endorse_suggestion(request, book_id, item_id): +def endorse_suggestion(request: HttpRequest, book_id: int, item_id: int) -> Any: """endorse a suggestion""" item = get_object_or_404( models.SuggestionListItem, id=item_id, book_list__suggests_for=book_id diff --git a/mypy.ini b/mypy.ini index 600c370e4a..181a1588aa 100644 --- a/mypy.ini +++ b/mypy.ini @@ -22,6 +22,12 @@ ignore_errors = False [mypy-bookwyrm.isbn.*] ignore_errors = False +[mypy-bookwyrm.views.suggestion_list] +ignore_errors = False +allow_untyped_calls = True +disable_error_code = assignment + + [mypy-celerywyrm.*] ignore_errors = False From b5b9a4fb6433ad4b90a99a5aa43511b9bb63095d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 19:21:55 -0700 Subject: [PATCH 41/52] Fixes placement of ignores that black muddled (rude) --- bookwyrm/views/suggestion_list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/views/suggestion_list.py b/bookwyrm/views/suggestion_list.py index f8b6cb7558..85c3bd9606 100644 --- a/bookwyrm/views/suggestion_list.py +++ b/bookwyrm/views/suggestion_list.py @@ -5,7 +5,7 @@ from django.core.paginator import Paginator from django.db import transaction from django.db.models import Count, Q -from django.http import HttpRequest, HttpResponse +from django.http import HttpRequest from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.urls import reverse @@ -84,8 +84,8 @@ def get( @method_decorator(login_required, name="dispatch") def post( - self, request: HttpRequest, book_id: int - ) -> Any: # pylint: disable=unused-argument + self, request: HttpRequest, book_id: int # pylint: disable=unused-argument + ) -> Any: """create a suggestion_list""" form = forms.SuggestionListForm(request.POST) From 5d5f89ff5d188d77336f9ad3fa316915a90de2a7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 15 Oct 2025 11:14:35 -0700 Subject: [PATCH 42/52] Updates migration path --- ...stitem_options_suggestionlist_and_more.py} | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) rename bookwyrm/migrations/{0210_suggestionlist_alter_listitem_options_and_more.py => 0219_alter_listitem_options_suggestionlist_and_more.py} (84%) diff --git a/bookwyrm/migrations/0210_suggestionlist_alter_listitem_options_and_more.py b/bookwyrm/migrations/0219_alter_listitem_options_suggestionlist_and_more.py similarity index 84% rename from bookwyrm/migrations/0210_suggestionlist_alter_listitem_options_and_more.py rename to bookwyrm/migrations/0219_alter_listitem_options_suggestionlist_and_more.py index 1864701dec..24146ecfe0 100644 --- a/bookwyrm/migrations/0210_suggestionlist_alter_listitem_options_and_more.py +++ b/bookwyrm/migrations/0219_alter_listitem_options_suggestionlist_and_more.py @@ -1,19 +1,23 @@ -# Generated by Django 4.2.15 on 2024-08-27 17:27 +# Generated by Django 5.2.3 on 2025-10-15 18:14 import bookwyrm.models.activitypub_mixin import bookwyrm.models.fields +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ("bookwyrm", "0209_user_show_ratings"), + ("bookwyrm", "0218_merge_0217_merge_20250816_0749_0217_usersession"), ] operations = [ + migrations.AlterModelOptions( + name="listitem", + options={}, + ), migrations.CreateModel( name="SuggestionList", fields=[ @@ -50,6 +54,21 @@ class Migration(migrations.Migration): max_length=255, ), ), + ( + "suggests_for", + bookwyrm.models.fields.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="suggestion_list", + to="bookwyrm.work", + ), + ), + ( + "user", + bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ "ordering": ("-updated_date",), @@ -60,10 +79,6 @@ class Migration(migrations.Migration): models.Model, ), ), - migrations.AlterModelOptions( - name="listitem", - options={}, - ), migrations.CreateModel( name="SuggestionListItem", fields=[ @@ -131,23 +146,9 @@ class Migration(migrations.Migration): model_name="suggestionlist", name="books", field=models.ManyToManyField( - through="bookwyrm.SuggestionListItem", to="bookwyrm.edition" - ), - ), - migrations.AddField( - model_name="suggestionlist", - name="suggests_for", - field=bookwyrm.models.fields.OneToOneField( - on_delete=django.db.models.deletion.PROTECT, - related_name="suggestion_list", - to="bookwyrm.work", - ), - ), - migrations.AddField( - model_name="suggestionlist", - name="user", - field=bookwyrm.models.fields.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL + through="bookwyrm.SuggestionListItem", + through_fields=("book_list", "book"), + to="bookwyrm.edition", ), ), ] From edfdfd25e4b3993e2730f8aa55be923ce73b11b4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 15 Oct 2025 11:58:13 -0700 Subject: [PATCH 43/52] Updates UI for better handling of null and logged out states --- bookwyrm/templates/book/suggestion_list/list.html | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index efda10e555..7749ab74b1 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -1,11 +1,14 @@ {% load i18n %} {% load humanize %} -

    +{% if request.user.is_authenticated or suggestion_list %} +

    {% trans "Suggestions" %} + {% if suggestion_list and items.length > 0 %} {% trans "View all suggestions" %} + {% endif %}

    {% if suggestion_list %} @@ -35,12 +38,15 @@

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

    {% trans "Have a recommendation for someone who liked this book?" %}

    +
    {% endif %} +{% endif %} From 45486e632bd8e99747a403d98b7d5bf87f204d06 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 15 Oct 2025 12:18:51 -0700 Subject: [PATCH 44/52] Fixes suggestion mode in regular list for for suggestion list --- bookwyrm/templates/book/suggestion_list/list.html | 2 +- bookwyrm/templates/lists/list.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index 7749ab74b1..cd066ae08d 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -4,7 +4,7 @@ {% if request.user.is_authenticated or suggestion_list %}

    {% trans "Suggestions" %} - {% if suggestion_list and items.length > 0 %} + {% if suggestion_list and items|length > 0 %} {% trans "View all suggestions" %} diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index d5f6d5fd09..e1f5fcd9e1 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -172,7 +172,7 @@

    {% trans "Suggest Books" %} {% endif %}

    - {% include "lists/suggestion_search.html" with query_param="q" search_url=request.path %} + {% include "lists/suggestion_search.html" with query_param="q" search_url=request.path is_suggestion=list.suggests_for %} {% endif %}

    From e8278fc2c991c0a7fd3dc686f13fcf103d348d1f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 15 Oct 2025 12:41:37 -0700 Subject: [PATCH 45/52] Show number of items by "view all" link --- bookwyrm/templates/book/suggestion_list/list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index cd066ae08d..e264b83616 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -6,7 +6,7 @@

    {% trans "Suggestions" %} {% if suggestion_list and items|length > 0 %} - {% trans "View all suggestions" %} + {% trans "View all suggestions" %} ({{ items|length }}) {% endif %}

    From 0725dae35c470fa016dfa22679f9b1fe78a35725 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 15 Oct 2025 14:01:02 -0700 Subject: [PATCH 46/52] Adds inbox test and tweaks serialization --- bookwyrm/activitypub/__init__.py | 2 +- bookwyrm/activitypub/ordered_collection.py | 12 +++- bookwyrm/models/list.py | 1 + bookwyrm/tests/views/inbox/test_inbox_add.py | 59 ++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 0705dc2410..bd33ece2fc 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -14,7 +14,7 @@ from .note import Review, Rating from .note import Tombstone from .ordered_collection import OrderedCollection, OrderedCollectionPage -from .ordered_collection import CollectionItem, ListItem, ShelfItem +from .ordered_collection import CollectionItem, ListItem, ShelfItem, SuggestionListItem from .ordered_collection import BookList, SuggestionList, Shelf from .person import Person, PublicKey from .response import ActivitypubResponse diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index a88d373287..b844566f2a 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -3,6 +3,7 @@ from typing import List from .base_activity import ActivityObject +from .book import Work # pylint: disable=invalid-name @@ -47,7 +48,7 @@ class SuggestionList(OrderedCollectionPrivate): """structure of an ordered collection activity""" summary: str = None - book: str = None + book: Work = None type: str = "SuggestionList" @@ -82,6 +83,15 @@ class ListItem(CollectionItem): type: str = "ListItem" +@dataclass(init=False) +class SuggestionListItem(CollectionItem): + """a book on a list""" + + book: str + notes: str = None + type: str = "SuggestionListItem" + + @dataclass(init=False) class ShelfItem(CollectionItem): """a book on a list""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 1becf8f080..50537d7c42 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -280,3 +280,4 @@ class SuggestionListItem(AbstractListItem): book_list = models.ForeignKey("SuggestionList", on_delete=models.CASCADE) endorsement = models.ManyToManyField("User", related_name="suggestion_endorsers") + activity_serializer = activitypub.SuggestionListItem diff --git a/bookwyrm/tests/views/inbox/test_inbox_add.py b/bookwyrm/tests/views/inbox/test_inbox_add.py index b3d50fe46f..31fb9f2a33 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_add.py +++ b/bookwyrm/tests/views/inbox/test_inbox_add.py @@ -44,6 +44,11 @@ def setUpTestData(cls): remote_id="https://example.com/book/37292", parent_work=work, ) + cls.another_book = models.Edition.objects.create( + title="Another Test", + remote_id="https://example.com/book/79", + parent_work=models.Work.objects.create(title="blah"), + ) models.SiteSettings.objects.create() @@ -134,3 +139,57 @@ def test_handle_add_book_to_list(self): self.assertEqual(booklist.books.first(), self.book) self.assertEqual(listitem.remote_id, "https://example.com/listbook/6189") self.assertEqual(listitem.notes, "hi hello") + + @responses.activate + def test_handle_add_book_to_suggestion_list(self): + """listing a book""" + responses.add( + responses.GET, + "https://example.com/book/suggestion/list", + json={ + "id": "https://example.com/book/{self.another_book.id}/suggestions", + "type": "SuggestionList", + "totalItems": 1, + "book": self.another_book.parent_work.to_activity(), + "first": f"https://example.com/book/{self.another_book.id}/suggestions?page=1", + "last": f"https://example.com/book/{self.another_book.id}/suggestions?page=1", + "name": "Test List", + "owner": "https://example.com/user/mouse", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "summary": "summary text", + "curation": "open", + "@context": "https://www.w3.org/ns/activitystreams", + }, + ) + responses.add( + responses.GET, + self.another_book.parent_work.remote_id, + json=self.another_book.parent_work.to_activity(), + ) + + activity = { + "id": f"https://example.com/book/{self.another_book.id}/suggestions#add", + "type": "Add", + "actor": "https://example.com/users/rat", + "object": { + "actor": self.remote_user.remote_id, + "type": "SuggestionListItem", + "book": self.book.remote_id, + "id": f"https://example.com/list/suggestion/item", + "notes": "hi hello", + "order": 1, + }, + "target": "https://example.com/book/suggestion/list", + "@context": "https://www.w3.org/ns/activitystreams", + } + views.inbox.activity_task(activity) + + booklist = models.SuggestionList.objects.get() + listitem = models.SuggestionListItem.objects.get() + self.assertEqual( + booklist.name, f"Suggestions for {self.another_book.parent_work.title}" + ) + self.assertEqual(booklist.suggests_for, self.another_book.parent_work) + self.assertEqual(booklist.books.first(), self.book) + self.assertEqual(listitem.notes, "hi hello") From acb92806c166e83d9607114625b091c81f908c10 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 15 Oct 2025 14:09:36 -0700 Subject: [PATCH 47/52] Fixing linting issue --- bookwyrm/tests/views/inbox/test_inbox_add.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/tests/views/inbox/test_inbox_add.py b/bookwyrm/tests/views/inbox/test_inbox_add.py index 31fb9f2a33..aa8de2f2a0 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_add.py +++ b/bookwyrm/tests/views/inbox/test_inbox_add.py @@ -176,7 +176,7 @@ def test_handle_add_book_to_suggestion_list(self): "actor": self.remote_user.remote_id, "type": "SuggestionListItem", "book": self.book.remote_id, - "id": f"https://example.com/list/suggestion/item", + "id": "https://example.com/list/suggestion/item", "notes": "hi hello", "order": 1, }, From 498d0de16106162646bdc48bbf4aeab3bc40dca2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 15 Oct 2025 14:27:24 -0700 Subject: [PATCH 48/52] Suppress line too long warning in suggestions test --- bookwyrm/tests/views/inbox/test_inbox_add.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bookwyrm/tests/views/inbox/test_inbox_add.py b/bookwyrm/tests/views/inbox/test_inbox_add.py index aa8de2f2a0..9305e1e36b 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_add.py +++ b/bookwyrm/tests/views/inbox/test_inbox_add.py @@ -140,6 +140,7 @@ def test_handle_add_book_to_list(self): self.assertEqual(listitem.remote_id, "https://example.com/listbook/6189") self.assertEqual(listitem.notes, "hi hello") + # pylint: disable=line-too-long @responses.activate def test_handle_add_book_to_suggestion_list(self): """listing a book""" From 73f437e559c667f034ea0d80077bd4c24fce6cad Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 16 Nov 2025 11:29:04 -0800 Subject: [PATCH 49/52] Adds merge migration --- bookwyrm/migrations/0224_merge_20251116_1928.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 bookwyrm/migrations/0224_merge_20251116_1928.py diff --git a/bookwyrm/migrations/0224_merge_20251116_1928.py b/bookwyrm/migrations/0224_merge_20251116_1928.py new file mode 100644 index 0000000000..94b6b1c279 --- /dev/null +++ b/bookwyrm/migrations/0224_merge_20251116_1928.py @@ -0,0 +1,13 @@ +# Generated by Django 5.2.3 on 2025-11-16 19:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0219_alter_listitem_options_suggestionlist_and_more"), + ("bookwyrm", "0223_sitesettings_disable_federation"), + ] + + operations = [] From b3bb96adfa487c938e4b8da206b39205afa0404e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 16 Nov 2025 11:38:49 -0800 Subject: [PATCH 50/52] Modifies language on "create" step of suggestion list --- bookwyrm/templates/book/suggestion_list/list.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bookwyrm/templates/book/suggestion_list/list.html b/bookwyrm/templates/book/suggestion_list/list.html index e264b83616..153d0ed6f7 100644 --- a/bookwyrm/templates/book/suggestion_list/list.html +++ b/bookwyrm/templates/book/suggestion_list/list.html @@ -11,16 +11,18 @@

    {% endif %}

    -{% if suggestion_list %} -

    {% blocktrans trimmed with title=book.title %} Readers who liked {{ title }} recommend giving these books a try: {% endblocktrans %}

    + +{% if suggestion_list %} + {% if items|length == 0 %}
    +

    {% trans "Have a recommendation for someone who liked this book?" %}

    {% include "book/suggestion_list/search.html" with list=suggestion_list is_suggestion=True %}
    @@ -40,12 +42,12 @@

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

    {% trans "Have a recommendation for someone who liked this book?" %}

    - +
    {% endif %} From e312fac681155ea0a83fc3c8a7169550e3985c62 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 16 Nov 2025 11:44:31 -0800 Subject: [PATCH 51/52] Adds shelve button to suggestion item Plus a title on the endorse button to clarify what it is --- bookwyrm/templates/book/suggestion_list/book_card.html | 3 +++ .../templates/book/suggestion_list/endorsement_button.html | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/book/suggestion_list/book_card.html b/bookwyrm/templates/book/suggestion_list/book_card.html index a2ead516a6..e0e732ff90 100644 --- a/bookwyrm/templates/book/suggestion_list/book_card.html +++ b/bookwyrm/templates/book/suggestion_list/book_card.html @@ -35,6 +35,9 @@ {% endblocktrans %}

    + diff --git a/bookwyrm/templates/book/suggestion_list/endorsement_button.html b/bookwyrm/templates/book/suggestion_list/endorsement_button.html index 9ddad536cd..6d5bdb82f7 100644 --- a/bookwyrm/templates/book/suggestion_list/endorsement_button.html +++ b/bookwyrm/templates/book/suggestion_list/endorsement_button.html @@ -1,7 +1,7 @@ {% load i18n %}
    {% csrf_token %} -