From 2faf5cea2a723c43b09b5ffba164e951b23f9682 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 2 Mar 2021 09:01:31 -0800 Subject: [PATCH 01/54] modifies edit book code to allow creation as well --- bookwyrm/templates/edit_book.html | 80 +++++++++++++++++++------------ bookwyrm/urls.py | 1 + bookwyrm/views/books.py | 34 ++++++++----- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index 4d215949..aab9e103 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -2,12 +2,16 @@ {% load i18n %} {% load humanize %} -{% block title %}{% trans "Edit Book" %}{% endblock %} +{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %} {% block content %}

- Edit "{{ book.title }}" + {% if book %} + {% blocktrans with book_title=book.title %}Edit "{{ book.title }}"{% endblocktrans %} + {% else %} + {% trans "Add Book" %} + {% endif %}

{% trans "Added:" %} {{ book.created_date | naturaltime }}

@@ -27,35 +31,49 @@
-

{% trans "Metadata" %}

-

{{ form.title }}

- {% for error in form.title.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.subtitle }}

- {% for error in form.subtitle.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.description }}

- {% for error in form.description.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.series }}

- {% for error in form.series.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.series_number }}

- {% for error in form.series_number.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.first_published_date }}

- {% for error in form.first_published_date.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.published_date }}

- {% for error in form.published_date.errors %} -

{{ error | escape }}

- {% endfor %} +
+

{% trans "Metadata" %}

+

{{ form.title }}

+ {% for error in form.title.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.subtitle }}

+ {% for error in form.subtitle.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.description }}

+ {% for error in form.description.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.series }}

+ {% for error in form.series.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.series_number }}

+ {% for error in form.series_number.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.first_published_date }}

+ {% for error in form.first_published_date.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.published_date }}

+ {% for error in form.published_date.errors %} +

{{ error | escape }}

+ {% endfor %} +
+ +
+

{% trans "Authors" %}

+ {% for author in book.authors.all %} +

{{ author.name }} + + {% endfor %} + + +

diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index a741088a..dfb64c23 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -134,6 +134,7 @@ urlpatterns = [ re_path(r'^add-description/(?P\d+)/?$', views.add_description), re_path(r'^resolve-book/?$', views.resolve_book), re_path(r'^switch-edition/?$', views.switch_edition), + re_path(r'^create-book/?$', views.EditBook.as_view()), # author re_path(r'^author/(?P\d+)(.json)?/?$', views.Author.as_view()), diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 4d6afba9..1754982e 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -106,28 +106,40 @@ class Book(View): name='dispatch') class EditBook(View): ''' edit a book ''' - def get(self, request, book_id): + def get(self, request, book_id=None): ''' info about a book ''' - book = get_edition(book_id) - if not book.description: - book.description = book.parent_work.description + book = None + if book_id: + book = get_edition(book_id) + if not book.description: + book.description = book.parent_work.description data = { 'book': book, 'form': forms.EditionForm(instance=book) } return TemplateResponse(request, 'edit_book.html', data) - def post(self, request, book_id): + def post(self, request, book_id=None): ''' edit a book cool ''' - book = get_object_or_404(models.Edition, id=book_id) - + book = get_object_or_404(models.Edition, id=book_id) if book_id \ + else None form = forms.EditionForm(request.POST, request.FILES, instance=book) + + data = { + 'book': book, + 'form': form + } if not form.is_valid(): - data = { - 'book': book, - 'form': form - } return TemplateResponse(request, 'edit_book.html', data) + + if not book or form.author: + # creting a book or adding an author to a book needs another step + return TemplateResponse(request, 'confirm_book.html', data) + + # remove authors + if request.POST.get('remove-author'): + import pdb;pdb.set_trace() + author = get_object_or_404(id=author_id) book = form.save() return redirect('/book/%s' % book.id) From b2d1384bc5065725ec51c4204cc97f64a9a751f6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 4 Mar 2021 13:48:50 -0800 Subject: [PATCH 02/54] UI for adding and removing authors --- bookwyrm/templates/edit_book.html | 43 ++++++++++++++++++++++++++++--- bookwyrm/tests/views/test_book.py | 16 ++++++++++++ bookwyrm/views/books.py | 33 ++++++++++++++++++------ 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index aab9e103..d0eb50be 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -26,7 +26,8 @@
{% endif %} -
+ +
{% csrf_token %}
@@ -68,11 +69,11 @@ {% for author in book.authors.all %}

{{ author.name }} {% endfor %} - +

@@ -138,6 +139,42 @@ {% trans "Cancel" %}
+ +{% if author_matches or book_matches %} +
+
+

{% trans "Confirm Book Info" %}

+
+ {% if author_matches.exists %} +
+ {% blocktrans %}Is "{{ add_author }}" an existing author?{% endblocktrans %} + {% for match in author_matches %} + +

+ {% blocktrans with book_title=match.book_set.first.title %}Author of {{ book_title }}{% endblocktrans %} +

+ {% endfor %} + +
+ {% else %} +

{% blocktrans %}Creating a new author: {{ add_author }}{% endblocktrans %}

+ {% endif %} + + {% if not book %} +
+ {% trans "Is this an editions of an existing work?" %} + {% for match in book_matches %} + + {% endfor %} + +
+ {% endif %} +
+ + +
+{% endif %} + {% endblock %} diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index b3360200..b7eaac4b 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -83,6 +83,22 @@ class BookViews(TestCase): self.assertEqual(self.book.title, 'New Title') + def test_edit_book_add_author(self): + ''' lets a user edit a book ''' + view = views.EditBook.as_view() + self.local_user.groups.add(self.group) + form = forms.EditionForm(instance=self.book) + form.data['title'] = 'New Title' + form.data['last_edited_by'] = self.local_user.id + form.data['add_author'] = "John Doe" + request = self.factory.post('', form.data) + request.user = self.local_user + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + view(request, self.book.id) + self.book.refresh_from_db() + self.assertEqual(self.book.title, 'New Title') + + def test_switch_edition(self): ''' updates user's relationships to a book ''' work = models.Work.objects.create(title='test work') diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 1754982e..174ddaa3 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -1,6 +1,7 @@ ''' the good stuff! the books! ''' -from django.core.paginator import Paginator from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.postgres.search import SearchRank, SearchVector +from django.core.paginator import Paginator from django.db import transaction from django.db.models import Avg, Q from django.http import HttpResponseNotFound @@ -132,16 +133,32 @@ class EditBook(View): if not form.is_valid(): return TemplateResponse(request, 'edit_book.html', data) - if not book or form.author: + add_author = request.POST.get('add_author') + if not book or add_author: # creting a book or adding an author to a book needs another step - return TemplateResponse(request, 'confirm_book.html', data) + data['confirm_mode'] = True + data['add_author'] = add_author + # check for existing authors + vector = SearchVector('name', weight='A') +\ + SearchVector('aliases', weight='B') + + data['author_matches'] = models.Author.objects.annotate( + search=vector + ).annotate( + rank=SearchRank(vector, add_author) + ).filter(rank__gt=0.8).order_by('-rank')[:5] + + # check if this is an edition of an existing work + author_text = book.author_text if book else add_author + data['book_matches'] = connector_manager.local_search( + '%s %s' % (form.cleaned_data.get('title'), author_text), + min_confidence=0.5, + raw=True + )[:5] + + return TemplateResponse(request, 'edit_book.html', data) - # remove authors - if request.POST.get('remove-author'): - import pdb;pdb.set_trace() - author = get_object_or_404(id=author_id) book = form.save() - return redirect('/book/%s' % book.id) From 5c089db086f6d85748cfff84ea04d54aababd5fa Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 4 Mar 2021 17:09:49 -0800 Subject: [PATCH 03/54] Full add author flow --- bookwyrm/templates/edit_book.html | 84 +++++++++++++++++-------------- bookwyrm/tests/views/test_book.py | 13 +---- bookwyrm/urls.py | 1 + bookwyrm/views/__init__.py | 2 +- bookwyrm/views/books.py | 53 +++++++++++++++++-- 5 files changed, 96 insertions(+), 57 deletions(-) diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index d0eb50be..0637bb79 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -8,7 +8,7 @@

{% if book %} - {% blocktrans with book_title=book.title %}Edit "{{ book.title }}"{% endblocktrans %} + {% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %} {% else %} {% trans "Add Book" %} {% endif %} @@ -26,9 +26,46 @@

{% endif %} -
-
+ {% csrf_token %} + {% if confirm_mode %} +
+

{% trans "Confirm Book Info" %}

+
+ {% if author_matches.exists %} +
+ {% blocktrans with name=add_author %}Is "{{ name }}" an existing author?{% endblocktrans %} + {% for match in author_matches %} + +

+ {% blocktrans with book_title=match.book_set.first.title %}Author of {{ book_title }}{% endblocktrans %} +

+ {% endfor %} + +
+ {% else %} +

{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}

+ {% endif %} + + {% if not book %} +
+ {% trans "Is this an editions of an existing work?" %} + {% for match in book_matches %} + + {% endfor %} + +
+ {% endif %} +
+ + + +
+ +
+ {% endif %} + +
@@ -72,8 +109,12 @@ {% trans "Remove this author" %} {% endfor %} + {% if confirm_mode %} + + {% else %} - + + {% endif %}
@@ -142,39 +183,4 @@
-{% if author_matches or book_matches %} -
-
-

{% trans "Confirm Book Info" %}

-
- {% if author_matches.exists %} -
- {% blocktrans %}Is "{{ add_author }}" an existing author?{% endblocktrans %} - {% for match in author_matches %} - -

- {% blocktrans with book_title=match.book_set.first.title %}Author of {{ book_title }}{% endblocktrans %} -

- {% endfor %} - -
- {% else %} -

{% blocktrans %}Creating a new author: {{ add_author }}{% endblocktrans %}

- {% endif %} - - {% if not book %} -
- {% trans "Is this an editions of an existing work?" %} - {% for match in book_matches %} - - {% endfor %} - -
- {% endif %} -
- - -
-{% endif %} - {% endblock %} diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index b7eaac4b..8a6c66cb 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -85,18 +85,7 @@ class BookViews(TestCase): def test_edit_book_add_author(self): ''' lets a user edit a book ''' - view = views.EditBook.as_view() - self.local_user.groups.add(self.group) - form = forms.EditionForm(instance=self.book) - form.data['title'] = 'New Title' - form.data['last_edited_by'] = self.local_user.id - form.data['add_author'] = "John Doe" - request = self.factory.post('', form.data) - request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - view(request, self.book.id) - self.book.refresh_from_db() - self.assertEqual(self.book.title, 'New Title') + # TODO def test_switch_edition(self): diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index dfb64c23..9e0d4630 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -129,6 +129,7 @@ urlpatterns = [ # books re_path(r'%s(.json)?/?$' % book_path, views.Book.as_view()), re_path(r'%s/edit/?$' % book_path, views.EditBook.as_view()), + re_path(r'%s/confirm/?$' % book_path, views.ConfirmEditBook.as_view()), re_path(r'%s/editions(.json)?/?$' % book_path, views.Editions.as_view()), re_path(r'^upload-cover/(?P\d+)/?$', views.upload_cover), re_path(r'^add-description/(?P\d+)/?$', views.add_description), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 2c7cdc46..c3d17da7 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -2,7 +2,7 @@ from .authentication import Login, Register, Logout from .author import Author, EditAuthor from .block import Block, unblock -from .books import Book, EditBook, Editions +from .books import Book, EditBook, ConfirmEditBook, Editions from .books import upload_cover, add_description, switch_edition, resolve_book from .error import not_found_page, server_error_page from .federation import Federation diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 174ddaa3..0097eb9f 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -122,8 +122,8 @@ class EditBook(View): def post(self, request, book_id=None): ''' edit a book cool ''' - book = get_object_or_404(models.Edition, id=book_id) if book_id \ - else None + # returns None if no match is found + book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) data = { @@ -134,9 +134,8 @@ class EditBook(View): return TemplateResponse(request, 'edit_book.html', data) add_author = request.POST.get('add_author') - if not book or add_author: - # creting a book or adding an author to a book needs another step - data['confirm_mode'] = True + # we're adding an author through a free text field + if add_author: data['add_author'] = add_author # check for existing authors vector = SearchVector('name', weight='A') +\ @@ -148,6 +147,8 @@ class EditBook(View): rank=SearchRank(vector, add_author) ).filter(rank__gt=0.8).order_by('-rank')[:5] + # we're creating a new book + if not book: # check if this is an edition of an existing work author_text = book.author_text if book else add_author data['book_matches'] = connector_manager.local_search( @@ -156,12 +157,54 @@ class EditBook(View): raw=True )[:5] + # either of the above cases requires additional confirmation + if add_author or not book: + # creting a book or adding an author to a book needs another step + data['confirm_mode'] = True return TemplateResponse(request, 'edit_book.html', data) book = form.save() return redirect('/book/%s' % book.id) +@method_decorator(login_required, name='dispatch') +@method_decorator( + permission_required('bookwyrm.edit_book', raise_exception=True), + name='dispatch') +class ConfirmEditBook(View): + ''' confirm edits to a book ''' + def post(self, request, book_id=None): + ''' edit a book cool ''' + # returns None if no match is found + book = models.Edition.objects.filter(id=book_id).first() + form = forms.EditionForm(request.POST, request.FILES, instance=book) + + data = { + 'book': book, + 'form': form + } + if not form.is_valid(): + return TemplateResponse(request, 'edit_book.html', data) + + # create work, if needed + # TODO + + # save book + book = form.save() + + # get or create author as needed + if request.POST.get('add_author'): + if request.POST.get('author_match'): + author = get_object_or_404( + models.Author, id=request.POST['author_match']) + else: + author = models.Author.objects.create( + name=request.POST.get('add_author')) + book.authors.add(author) + + return redirect('/book/%s' % book.id) + + class Editions(View): ''' list of editions ''' def get(self, request, book_id): From bbd3ac7242662964b055c89344de25c745c77019 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 7 Mar 2021 13:17:11 -0800 Subject: [PATCH 04/54] Removes ID field from hideen form value --- bookwyrm/templates/edit_book.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index 0637bb79..dc927e8a 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -110,7 +110,7 @@ {% endfor %} {% if confirm_mode %} - + {% else %} From f1b699d8105c96dbb83f2b88102dffd75e83ddd0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 7 Mar 2021 13:59:27 -0800 Subject: [PATCH 05/54] Tests adding author to book --- bookwyrm/tests/views/test_book.py | 36 +++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index 8a6c66cb..0ba09e3b 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -84,8 +84,40 @@ class BookViews(TestCase): def test_edit_book_add_author(self): - ''' lets a user edit a book ''' - # TODO + ''' lets a user edit a book with new authors ''' + view = views.EditBook.as_view() + self.local_user.groups.add(self.group) + form = forms.EditionForm(instance=self.book) + form.data['title'] = 'New Title' + form.data['last_edited_by'] = self.local_user.id + form.data['add_author'] = 'Sappho' + request = self.factory.post('', form.data) + request.user = self.local_user + + result = view(request, self.book.id) + result.render() + + # the changes haven't been saved yet + self.book.refresh_from_db() + self.assertEqual(self.book.title, 'Example Edition') + + def test_edit_book_add_new_author_confirm(self): + ''' lets a user edit a book confirmed with new authors ''' + view = views.ConfirmEditBook.as_view() + self.local_user.groups.add(self.group) + form = forms.EditionForm(instance=self.book) + form.data['title'] = 'New Title' + form.data['last_edited_by'] = self.local_user.id + form.data['add_author'] = 'Sappho' + request = self.factory.post('', form.data) + request.user = self.local_user + + view(request, self.book.id) + + # the changes haven't been saved yet + self.book.refresh_from_db() + self.assertEqual(self.book.title, 'New Title') + self.assertEqual(self.book.authors.first().name, 'Sappho') def test_switch_edition(self): From 79d9c493f7fcc8b64d9e3c878f8358f331c29d11 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 7 Mar 2021 14:19:22 -0800 Subject: [PATCH 06/54] Remove author flow --- bookwyrm/templates/edit_book.html | 14 ++++++++------ bookwyrm/views/books.py | 4 ++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index dc927e8a..a4d62efd 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -103,12 +103,14 @@

{% trans "Authors" %}

- {% for author in book.authors.all %} -

{{ author.name }} - - {% endfor %} +

+ {% for author in book.authors.all %} +

{{ author.name }} + + {% endfor %} +

{% if confirm_mode %} {% else %} diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 0097eb9f..cc05a79c 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -163,6 +163,10 @@ class EditBook(View): data['confirm_mode'] = True return TemplateResponse(request, 'edit_book.html', data) + remove_authors = request.POST.getlist('remove_authors') + for author_id in remove_authors: + book.authors.remove(author_id) + book = form.save() return redirect('/book/%s' % book.id) From 1eac2b938618f6dcf8ed63989ed39b560e6cd826 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 7 Mar 2021 15:14:57 -0800 Subject: [PATCH 07/54] Test for deleting authors --- bookwyrm/tests/views/test_book.py | 20 +++++++++++++++++++- bookwyrm/views/books.py | 4 ++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index 0ba09e3b..6a28ba9d 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -114,11 +114,29 @@ class BookViews(TestCase): view(request, self.book.id) - # the changes haven't been saved yet self.book.refresh_from_db() self.assertEqual(self.book.title, 'New Title') self.assertEqual(self.book.authors.first().name, 'Sappho') + def test_edit_book_remove_author(self): + ''' remove an author from a book ''' + author = models.Author.objects.create(name='Sappho') + self.book.authors.add(author) + form = forms.EditionForm(instance=self.book) + view = views.EditBook.as_view() + self.local_user.groups.add(self.group) + form = forms.EditionForm(instance=self.book) + form.data['title'] = 'New Title' + form.data['last_edited_by'] = self.local_user.id + form.data['remove_authors'] = [author.id] + request = self.factory.post('', form.data) + request.user = self.local_user + + view(request, self.book.id) + self.book.refresh_from_db() + self.assertEqual(self.book.title, 'New Title') + self.assertFalse(self.book.authors.exists()) + def test_switch_edition(self): ''' updates user's relationships to a book ''' diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index cc05a79c..07a1d437 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -206,6 +206,10 @@ class ConfirmEditBook(View): name=request.POST.get('add_author')) book.authors.add(author) + remove_authors = request.POST.getlist('remove_authors') + for author_id in remove_authors: + book.authors.remove(author_id) + return redirect('/book/%s' % book.id) From d3162e12db9d4f9ae975253f5fe10e21747f6c0a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 08:51:54 -0800 Subject: [PATCH 08/54] Adds broadcast mock to edit book tests --- bookwyrm/tests/views/test_book.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index 6a28ba9d..484c695b 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -112,7 +112,8 @@ class BookViews(TestCase): request = self.factory.post('', form.data) request.user = self.local_user - view(request, self.book.id) + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + view(request, self.book.id) self.book.refresh_from_db() self.assertEqual(self.book.title, 'New Title') @@ -132,7 +133,8 @@ class BookViews(TestCase): request = self.factory.post('', form.data) request.user = self.local_user - view(request, self.book.id) + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + view(request, self.book.id) self.book.refresh_from_db() self.assertEqual(self.book.title, 'New Title') self.assertFalse(self.book.authors.exists()) From a5baa1f5c618e393b8f8e791c3730868044324da Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 09:28:22 -0800 Subject: [PATCH 09/54] Create new books --- bookwyrm/forms.py | 2 +- bookwyrm/templates/edit_book.html | 11 ++++--- bookwyrm/templates/search_results.html | 4 +++ bookwyrm/urls.py | 3 +- bookwyrm/views/books.py | 42 +++++++++++++++----------- 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index b920fc9c..1ab6e0ee 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -134,7 +134,7 @@ class EditionForm(CustomForm): 'updated_date', 'edition_rank', - 'authors',# TODO + 'authors', 'parent_work', 'shelves', diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index a4d62efd..1ebcc6b9 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -13,11 +13,13 @@ {% trans "Add Book" %} {% endif %} + {% if book %}

{% trans "Added:" %} {{ book.created_date | naturaltime }}

{% trans "Updated:" %} {{ book.updated_date | naturaltime }}

{% trans "Last edited by:" %} {{ book.last_edited_by.display_name }}

+ {% endif %}
{% if form.non_field_errors %} @@ -26,7 +28,12 @@ {% endif %} +{% if book %}
+{% else %} + +{% endif %} + {% csrf_token %} {% if confirm_mode %}
@@ -111,12 +118,8 @@ {% endfor %} - {% if confirm_mode %} - - {% else %} - {% endif %}
diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html index 4e8481f0..13497df8 100644 --- a/bookwyrm/templates/search_results.html +++ b/bookwyrm/templates/search_results.html @@ -68,6 +68,10 @@ {% include 'snippets/toggle/close_button.html' with text=button_text small=True controls_text="more-results" %} {% endif %} + + {% endif %}
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 25b44fb4..0000015e 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -130,12 +130,13 @@ urlpatterns = [ re_path(r'%s(.json)?/?$' % book_path, views.Book.as_view()), re_path(r'%s/edit/?$' % book_path, views.EditBook.as_view()), re_path(r'%s/confirm/?$' % book_path, views.ConfirmEditBook.as_view()), + re_path(r'^create-book/?$', views.EditBook.as_view()), + re_path(r'^create-book/confirm?$', views.ConfirmEditBook.as_view()), re_path(r'%s/editions(.json)?/?$' % book_path, views.Editions.as_view()), re_path(r'^upload-cover/(?P\d+)/?$', views.upload_cover), re_path(r'^add-description/(?P\d+)/?$', views.add_description), re_path(r'^resolve-book/?$', views.resolve_book), re_path(r'^switch-edition/?$', views.switch_edition), - re_path(r'^create-book/?$', views.EditBook.as_view()), # isbn re_path(r'^isbn/(?P\d+)(.json)?/?$', views.Isbn.as_view()), diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 5f03e1a9..a115ac49 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -145,7 +145,7 @@ class EditBook(View): search=vector ).annotate( rank=SearchRank(vector, add_author) - ).filter(rank__gt=0.8).order_by('-rank')[:5] + ).filter(rank__gt=0.4).order_by('-rank')[:5] # we're creating a new book if not book: @@ -190,25 +190,33 @@ class ConfirmEditBook(View): if not form.is_valid(): return TemplateResponse(request, 'edit_book.html', data) - # create work, if needed - # TODO + with transaction.atomic(): + # save book + book = form.save() - # save book - book = form.save() + # get or create author as needed + if request.POST.get('add_author'): + if request.POST.get('author_match'): + author = get_object_or_404( + models.Author, id=request.POST['author_match']) + else: + author = models.Author.objects.create( + name=request.POST.get('add_author')) + book.authors.add(author) - # get or create author as needed - if request.POST.get('add_author'): - if request.POST.get('author_match'): - author = get_object_or_404( - models.Author, id=request.POST['author_match']) - else: - author = models.Author.objects.create( - name=request.POST.get('add_author')) - book.authors.add(author) + # create work, if needed + if not book_id: + work_match = request.POST.get('parent_work') + if work_match: + work = get_object_or_404(models.Work, id=work_match) + else: + work = models.Work.objects.create(title=form.cleaned_data.title) + work.authors.set(book.authors.all()) + book.parent_work = work + book.save() - remove_authors = request.POST.getlist('remove_authors') - for author_id in remove_authors: - book.authors.remove(author_id) + for author_id in request.POST.getlist('remove_authors'): + book.authors.remove(author_id) return redirect('/book/%s' % book.id) From acbebbe94768bdefe763424ff157a6f3a13059bd Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 10:10:30 -0800 Subject: [PATCH 10/54] Formats code changes --- bookwyrm/forms.py | 1 - bookwyrm/tests/views/test_book.py | 45 ++++++++-------- bookwyrm/urls.py | 21 ++++---- bookwyrm/views/books.py | 87 +++++++++++++++---------------- bw-dev | 2 +- 5 files changed, 75 insertions(+), 81 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 09e13ae5..99c45ed2 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -138,7 +138,6 @@ class EditionForm(CustomForm): class Meta: model = models.Edition exclude = [ - "remote_id", "origin_id", "created_date", diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index feadf862..02493769 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -85,14 +85,14 @@ class BookViews(TestCase): self.assertEqual(self.book.title, "New Title") def test_edit_book_add_author(self): - ''' lets a user edit a book with new authors ''' + """ lets a user edit a book with new authors """ view = views.EditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) - form.data['title'] = 'New Title' - form.data['last_edited_by'] = self.local_user.id - form.data['add_author'] = 'Sappho' - request = self.factory.post('', form.data) + form.data["title"] = "New Title" + form.data["last_edited_by"] = self.local_user.id + form.data["add_author"] = "Sappho" + request = self.factory.post("", form.data) request.user = self.local_user result = view(request, self.book.id) @@ -100,47 +100,46 @@ class BookViews(TestCase): # the changes haven't been saved yet self.book.refresh_from_db() - self.assertEqual(self.book.title, 'Example Edition') + self.assertEqual(self.book.title, "Example Edition") def test_edit_book_add_new_author_confirm(self): - ''' lets a user edit a book confirmed with new authors ''' + """ lets a user edit a book confirmed with new authors """ view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) - form.data['title'] = 'New Title' - form.data['last_edited_by'] = self.local_user.id - form.data['add_author'] = 'Sappho' - request = self.factory.post('', form.data) + form.data["title"] = "New Title" + form.data["last_edited_by"] = self.local_user.id + form.data["add_author"] = "Sappho" + request = self.factory.post("", form.data) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): view(request, self.book.id) self.book.refresh_from_db() - self.assertEqual(self.book.title, 'New Title') - self.assertEqual(self.book.authors.first().name, 'Sappho') + self.assertEqual(self.book.title, "New Title") + self.assertEqual(self.book.authors.first().name, "Sappho") def test_edit_book_remove_author(self): - ''' remove an author from a book ''' - author = models.Author.objects.create(name='Sappho') + """ remove an author from a book """ + author = models.Author.objects.create(name="Sappho") self.book.authors.add(author) form = forms.EditionForm(instance=self.book) view = views.EditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) - form.data['title'] = 'New Title' - form.data['last_edited_by'] = self.local_user.id - form.data['remove_authors'] = [author.id] - request = self.factory.post('', form.data) + form.data["title"] = "New Title" + form.data["last_edited_by"] = self.local_user.id + form.data["remove_authors"] = [author.id] + request = self.factory.post("", form.data) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): view(request, self.book.id) self.book.refresh_from_db() - self.assertEqual(self.book.title, 'New Title') + self.assertEqual(self.book.title, "New Title") self.assertFalse(self.book.authors.exists()) - def test_switch_edition(self): """ updates user's relationships to a book """ work = models.Work.objects.create(title="test work") diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 246ac8af..ed752e75 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -117,17 +117,16 @@ urlpatterns = [ re_path(r"^boost/(?P\d+)/?$", views.Boost.as_view()), re_path(r"^unboost/(?P\d+)/?$", views.Unboost.as_view()), # books - re_path(r'%s(.json)?/?$' % book_path, views.Book.as_view()), - re_path(r'%s/edit/?$' % book_path, views.EditBook.as_view()), - re_path(r'%s/confirm/?$' % book_path, views.ConfirmEditBook.as_view()), - re_path(r'^create-book/?$', views.EditBook.as_view()), - re_path(r'^create-book/confirm?$', views.ConfirmEditBook.as_view()), - re_path(r'%s/editions(.json)?/?$' % book_path, views.Editions.as_view()), - re_path(r'^upload-cover/(?P\d+)/?$', views.upload_cover), - re_path(r'^add-description/(?P\d+)/?$', views.add_description), - re_path(r'^resolve-book/?$', views.resolve_book), - re_path(r'^switch-edition/?$', views.switch_edition), - + re_path(r"%s(.json)?/?$" % book_path, views.Book.as_view()), + re_path(r"%s/edit/?$" % book_path, views.EditBook.as_view()), + re_path(r"%s/confirm/?$" % book_path, views.ConfirmEditBook.as_view()), + re_path(r"^create-book/?$", views.EditBook.as_view()), + re_path(r"^create-book/confirm?$", views.ConfirmEditBook.as_view()), + re_path(r"%s/editions(.json)?/?$" % book_path, views.Editions.as_view()), + re_path(r"^upload-cover/(?P\d+)/?$", views.upload_cover), + re_path(r"^add-description/(?P\d+)/?$", views.add_description), + re_path(r"^resolve-book/?$", views.resolve_book), + re_path(r"^switch-edition/?$", views.switch_edition), # isbn re_path(r"^isbn/(?P\d+)(.json)?/?$", views.Isbn.as_view()), # author diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 7c3ec348..55cc75b4 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -106,107 +106,104 @@ class Book(View): permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" ) class EditBook(View): - ''' edit a book ''' + """ edit a book """ + def get(self, request, book_id=None): - ''' info about a book ''' + """ info about a book """ book = None if book_id: book = get_edition(book_id) if not book.description: book.description = book.parent_work.description - data = { - 'book': book, - 'form': forms.EditionForm(instance=book) - } - return TemplateResponse(request, 'edit_book.html', data) + data = {"book": book, "form": forms.EditionForm(instance=book)} + return TemplateResponse(request, "edit_book.html", data) def post(self, request, book_id=None): - ''' edit a book cool ''' + """ edit a book cool """ # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) - data = { - 'book': book, - 'form': form - } + data = {"book": book, "form": form} if not form.is_valid(): - return TemplateResponse(request, 'edit_book.html', data) + return TemplateResponse(request, "edit_book.html", data) - add_author = request.POST.get('add_author') + add_author = request.POST.get("add_author") # we're adding an author through a free text field if add_author: - data['add_author'] = add_author + data["add_author"] = add_author # check for existing authors - vector = SearchVector('name', weight='A') +\ - SearchVector('aliases', weight='B') + vector = SearchVector("name", weight="A") + SearchVector( + "aliases", weight="B" + ) - data['author_matches'] = models.Author.objects.annotate( - search=vector - ).annotate( - rank=SearchRank(vector, add_author) - ).filter(rank__gt=0.4).order_by('-rank')[:5] + data["author_matches"] = ( + models.Author.objects.annotate(search=vector) + .annotate(rank=SearchRank(vector, add_author)) + .filter(rank__gt=0.4) + .order_by("-rank")[:5] + ) # we're creating a new book if not book: # check if this is an edition of an existing work author_text = book.author_text if book else add_author - data['book_matches'] = connector_manager.local_search( - '%s %s' % (form.cleaned_data.get('title'), author_text), + data["book_matches"] = connector_manager.local_search( + "%s %s" % (form.cleaned_data.get("title"), author_text), min_confidence=0.5, - raw=True + raw=True, )[:5] # either of the above cases requires additional confirmation if add_author or not book: # creting a book or adding an author to a book needs another step - data['confirm_mode'] = True - return TemplateResponse(request, 'edit_book.html', data) + data["confirm_mode"] = True + return TemplateResponse(request, "edit_book.html", data) - remove_authors = request.POST.getlist('remove_authors') + remove_authors = request.POST.getlist("remove_authors") for author_id in remove_authors: book.authors.remove(author_id) book = form.save() - return redirect('/book/%s' % book.id) + return redirect("/book/%s" % book.id) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") @method_decorator( - permission_required('bookwyrm.edit_book', raise_exception=True), - name='dispatch') + permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" +) class ConfirmEditBook(View): - ''' confirm edits to a book ''' + """ confirm edits to a book """ + def post(self, request, book_id=None): - ''' edit a book cool ''' + """ edit a book cool """ # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) - data = { - 'book': book, - 'form': form - } + data = {"book": book, "form": form} if not form.is_valid(): - return TemplateResponse(request, 'edit_book.html', data) + return TemplateResponse(request, "edit_book.html", data) with transaction.atomic(): # save book book = form.save() # get or create author as needed - if request.POST.get('add_author'): - if request.POST.get('author_match'): + if request.POST.get("add_author"): + if request.POST.get("author_match"): author = get_object_or_404( - models.Author, id=request.POST['author_match']) + models.Author, id=request.POST["author_match"] + ) else: author = models.Author.objects.create( - name=request.POST.get('add_author')) + name=request.POST.get("add_author") + ) book.authors.add(author) # create work, if needed if not book_id: - work_match = request.POST.get('parent_work') + work_match = request.POST.get("parent_work") if work_match: work = get_object_or_404(models.Work, id=work_match) else: @@ -215,7 +212,7 @@ class ConfirmEditBook(View): book.parent_work = work book.save() - for author_id in request.POST.getlist('remove_authors'): + for author_id in request.POST.getlist("remove_authors"): book.authors.remove(author_id) return redirect("/book/%s" % book.id) diff --git a/bw-dev b/bw-dev index 74c42fbb..712b8028 100755 --- a/bw-dev +++ b/bw-dev @@ -36,7 +36,7 @@ function initdb { } function makeitblack { - runweb black celerywyrm bookwyrm + docker-compose run --rm web black celerywyrm bookwyrm } CMD=$1 From 58b48faff870723a8aba0bf745756c82c266c6ed Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 10:48:45 -0800 Subject: [PATCH 11/54] Tests create books flow --- bookwyrm/tests/views/test_book.py | 29 +++++++++++++++++++++++++++++ bookwyrm/views/books.py | 3 +-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index 02493769..90b7359d 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -140,6 +140,35 @@ class BookViews(TestCase): self.assertEqual(self.book.title, "New Title") self.assertFalse(self.book.authors.exists()) + def test_create_book(self): + """ create an entirely new book and work """ + view = views.ConfirmEditBook.as_view() + self.local_user.groups.add(self.group) + form = forms.EditionForm() + form.data["title"] = "New Title" + form.data["last_edited_by"] = self.local_user.id + request = self.factory.post("", form.data) + request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request) + book = models.Edition.objects.get(title="New Title") + self.assertEqual(book.parent_work.title, "New Title") + + def test_create_book_existing_work(self): + """ create an entirely new book and work """ + view = views.ConfirmEditBook.as_view() + self.local_user.groups.add(self.group) + form = forms.EditionForm() + form.data["title"] = "New Title" + form.data["parent_work"] = self.work.id + form.data["last_edited_by"] = self.local_user.id + request = self.factory.post("", form.data) + request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request) + book = models.Edition.objects.get(title="New Title") + self.assertEqual(book.parent_work, self.work) + def test_switch_edition(self): """ updates user's relationships to a book """ work = models.Work.objects.create(title="test work") diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 55cc75b4..ecba4376 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -1,5 +1,4 @@ """ the good stuff! the books! """ -from django.core.paginator import Paginator from django.contrib.auth.decorators import login_required, permission_required from django.contrib.postgres.search import SearchRank, SearchVector from django.core.paginator import Paginator @@ -207,7 +206,7 @@ class ConfirmEditBook(View): if work_match: work = get_object_or_404(models.Work, id=work_match) else: - work = models.Work.objects.create(title=form.cleaned_data.title) + work = models.Work.objects.create(title=form.cleaned_data["title"]) work.authors.set(book.authors.all()) book.parent_work = work book.save() From 37e29cc735f8b4c01fb225ab78265656f145bca7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 11:11:05 -0800 Subject: [PATCH 12/54] Adds tests of creating book with author --- bookwyrm/tests/views/test_book.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index 90b7359d..bba83714 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -169,6 +169,23 @@ class BookViews(TestCase): book = models.Edition.objects.get(title="New Title") self.assertEqual(book.parent_work, self.work) + def test_create_book_with_author(self): + """ create an entirely new book and work """ + view = views.ConfirmEditBook.as_view() + self.local_user.groups.add(self.group) + form = forms.EditionForm() + form.data["title"] = "New Title" + form.data["add_author"] = "Sappho" + form.data["last_edited_by"] = self.local_user.id + request = self.factory.post("", form.data) + request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request) + book = models.Edition.objects.get(title="New Title") + self.assertEqual(book.parent_work.title, "New Title") + self.assertEqual(book.authors.first().name, "Sappho") + self.assertEqual(book.authors.first(), book.parent_work.authors.first()) + def test_switch_edition(self): """ updates user's relationships to a book """ work = models.Work.objects.create(title="test work") From 500394fc52bc5b75393d5632fa6412c01fa244ac Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 11:54:26 -0800 Subject: [PATCH 13/54] Make sure creating books doesn't broadcast in tests --- bookwyrm/tests/views/test_book.py | 12 ++++++------ bookwyrm/views/books.py | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index bba83714..1549bdc6 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -149,8 +149,8 @@ class BookViews(TestCase): form.data["last_edited_by"] = self.local_user.id request = self.factory.post("", form.data) request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - view(request) + + view(request) book = models.Edition.objects.get(title="New Title") self.assertEqual(book.parent_work.title, "New Title") @@ -164,8 +164,8 @@ class BookViews(TestCase): form.data["last_edited_by"] = self.local_user.id request = self.factory.post("", form.data) request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - view(request) + + view(request) book = models.Edition.objects.get(title="New Title") self.assertEqual(book.parent_work, self.work) @@ -179,8 +179,8 @@ class BookViews(TestCase): form.data["last_edited_by"] = self.local_user.id request = self.factory.post("", form.data) request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - view(request) + + view(request) book = models.Edition.objects.get(title="New Title") self.assertEqual(book.parent_work.title, "New Title") self.assertEqual(book.authors.first().name, "Sappho") diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index ecba4376..ae60c677 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -209,7 +209,8 @@ class ConfirmEditBook(View): work = models.Work.objects.create(title=form.cleaned_data["title"]) work.authors.set(book.authors.all()) book.parent_work = work - book.save() + # we don't tell the world when creating a book + book.save(broadcast=False) for author_id in request.POST.getlist("remove_authors"): book.authors.remove(author_id) From a29d6a5f16ba1f0f75d0d283c4d01917bc0e4d82 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 14:11:08 -0800 Subject: [PATCH 14/54] Hide secondary save button in confirm mode --- bookwyrm/templates/edit_book.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index 1ebcc6b9..700dc570 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -72,7 +72,6 @@
{% endif %} -
@@ -181,11 +180,12 @@
+ {% if not confirm_mode %}
{% trans "Cancel" %}
-
+ {% endif %} {% endblock %} From ab57b5b906641df96be76fee71186d8eb4b51896 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 15:49:44 -0800 Subject: [PATCH 15/54] First pass at views for reporting --- bookwyrm/templates/layout.html | 4 +- bookwyrm/templates/settings/admin_layout.html | 4 ++ .../templates/settings/report_preview.html | 14 +++++++ bookwyrm/templates/settings/reports.html | 27 +++++++++++++ .../snippets/status/status_options.html | 5 +++ bookwyrm/templates/snippets/user_options.html | 3 ++ bookwyrm/urls.py | 7 ++++ bookwyrm/views/__init__.py | 3 +- bookwyrm/views/reports.py | 38 +++++++++++++++++++ 9 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 bookwyrm/templates/settings/report_preview.html create mode 100644 bookwyrm/templates/settings/reports.html create mode 100644 bookwyrm/views/reports.py diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 901a12ff..fc2ebdb7 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -114,8 +114,8 @@ {% endif %} {% if perms.bookwyrm.edit_instance_settings %}
  • - - {% trans 'Site Configuration' %} + + {% trans 'Admin' %}
  • {% endif %} diff --git a/bookwyrm/templates/settings/admin_layout.html b/bookwyrm/templates/settings/admin_layout.html index 16741436..312d502a 100644 --- a/bookwyrm/templates/settings/admin_layout.html +++ b/bookwyrm/templates/settings/admin_layout.html @@ -18,6 +18,10 @@ {% url 'settings-invites' as url %} {% trans "Invites" %} +
  • + {% url 'settings-reports' as url %} + {% trans "Reports" %} +
  • {% url 'settings-federation' as url %} {% trans "Federated Servers" %} diff --git a/bookwyrm/templates/settings/report_preview.html b/bookwyrm/templates/settings/report_preview.html new file mode 100644 index 00000000..67bffe27 --- /dev/null +++ b/bookwyrm/templates/settings/report_preview.html @@ -0,0 +1,14 @@ +{% extends 'components/card.html' %} +{% load i18n %} +{% block card-header %} +

    + report title

    +{% endblock %} + +{% block card-content %} +about this report +{% endblock %} + +{% block card-footer %} +footer +{% endblock diff --git a/bookwyrm/templates/settings/reports.html b/bookwyrm/templates/settings/reports.html new file mode 100644 index 00000000..efca7244 --- /dev/null +++ b/bookwyrm/templates/settings/reports.html @@ -0,0 +1,27 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} + +{% block header %}{% trans "Reports" %}{% endblock %} + +{% block panel %} + + +
      + {% for report in reports %} +
    • + {% include 'settings/report_preview.html' with report=report %} +
    • + {% endfor %} +
    + +{% endblock %} + diff --git a/bookwyrm/templates/snippets/status/status_options.html b/bookwyrm/templates/snippets/status/status_options.html index babd8296..0f099e17 100644 --- a/bookwyrm/templates/snippets/status/status_options.html +++ b/bookwyrm/templates/snippets/status/status_options.html @@ -10,6 +10,7 @@ {% block dropdown-list %} {% if status.user == request.user %} +{# things you can do to your own statuses #}
  • {% else %} +{# things you can do to other people's statuses #}
  • {% trans "Send direct message" %}
  • +
  • + {% trans "Report status" %} +
  • {% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
  • diff --git a/bookwyrm/templates/snippets/user_options.html b/bookwyrm/templates/snippets/user_options.html index b3f66782..c9fdac0a 100644 --- a/bookwyrm/templates/snippets/user_options.html +++ b/bookwyrm/templates/snippets/user_options.html @@ -12,6 +12,9 @@
  • {% trans "Send direct message" %}
  • +
  • + {% trans "Report user" %} +
  • {% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
  • diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 844f8993..c7ec7f4b 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -54,6 +54,13 @@ urlpatterns = [ re_path( r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites" ), + # moderation + re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"), + re_path( + r"^settings/report/(?P\d+)/?$", + views.Report.as_view(), + name="settings-report", + ), re_path(r"^invite/(?P[A-Za-z0-9]+)/?$", views.Invite.as_view()), # landing pages re_path(r"^about/?$", views.About.as_view()), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 48da8ec1..36a64ddf 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -20,15 +20,16 @@ from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import start_reading, finish_reading, delete_progressupdate +from .reports import Report, Reports from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword -from .tag import Tag, AddTag, RemoveTag from .search import Search from .shelf import Shelf from .shelf import user_shelves_page, create_shelf, delete_shelf from .shelf import shelve, unshelve from .site import Site from .status import CreateStatus, DeleteStatus +from .tag import Tag, AddTag, RemoveTag from .updates import Updates from .user import User, EditUser, Followers, Following from .isbn import Isbn diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py new file mode 100644 index 00000000..a90fffaf --- /dev/null +++ b/bookwyrm/views/reports.py @@ -0,0 +1,38 @@ +""" moderation via flagged posts and users """ +from django.contrib.auth.decorators import login_required, permission_required +from django.shortcuts import get_object_or_404 +from django.template.response import TemplateResponse +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import models + + +# pylint: disable= no-self-use +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("bookwyrm.moderate_user", raise_exception=True), + name="dispatch", +) +@method_decorator( + permission_required("bookwyrm.moderate_post", raise_exception=True), + name="dispatch", +) +class Reports(View): + """ list of reports """ + + def get(self, request, status="open"): + """ view current reports """ + data = { + "status": status + } # {"reports": models.Report.objects.filter(status=status)} + return TemplateResponse(request, "settings/reports.html", data) + + +class Report(View): + """ view a specific report """ + + def get(self, request, report_id): + """ load a report """ + data = {"report": get_object_or_404(models.REport, id=report_id)} + return TemplateResponse(request, "settings/report.html", data) From 7337a357fa676459634cab18c47b9c3a2fd6690c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 16:02:16 -0800 Subject: [PATCH 16/54] Adds tests file --- bookwyrm/tests/views/test_reports.py | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 bookwyrm/tests/views/test_reports.py diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py new file mode 100644 index 00000000..3b8dec26 --- /dev/null +++ b/bookwyrm/tests/views/test_reports.py @@ -0,0 +1,48 @@ +""" test for app action functionality """ +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models +from bookwyrm import views + + +class ReportViews(TestCase): + """ every response to a get request, html or json """ + + def setUp(self): + """ we need basic test data and mocks """ + self.factory = RequestFactory() + self.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) + models.SiteSettings.objects.create() + + def test_reports_page(self): + """ there are so many views, this just makes sure it LOADS """ + view = views.Reports.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + result = view(request) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + def test_report_page(self): + """ there are so many views, this just makes sure it LOADS """ + view = views.Report.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + report = models.Report.objects.create() + + result = view(request, report.id) + + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) From e59c12768642a9ebaa810a138972eebf6098c6b7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 17:16:59 -0800 Subject: [PATCH 17/54] Adds models for reporting --- .gitignore | 3 +- .../migrations/0049_report_reportcomment.py | 48 +++++++++++++++++++ bookwyrm/models/__init__.py | 1 + bookwyrm/models/federated_server.py | 2 +- bookwyrm/models/report.py | 23 +++++++++ bookwyrm/tests/views/test_reports.py | 9 +++- bookwyrm/views/reports.py | 12 +++-- 7 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 bookwyrm/migrations/0049_report_reportcomment.py create mode 100644 bookwyrm/models/report.py diff --git a/.gitignore b/.gitignore index 1384056f..4b5b7fef 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /venv *.pyc *.swp +**/__pycache__ # VSCode /.vscode @@ -15,4 +16,4 @@ /images/ # Testing -.coverage \ No newline at end of file +.coverage diff --git a/bookwyrm/migrations/0049_report_reportcomment.py b/bookwyrm/migrations/0049_report_reportcomment.py new file mode 100644 index 00000000..deb8ba6f --- /dev/null +++ b/bookwyrm/migrations/0049_report_reportcomment.py @@ -0,0 +1,48 @@ +# Generated by Django 3.0.7 on 2021-03-09 00:55 + +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0048_merge_20210308_1754'), + ] + + operations = [ + migrations.CreateModel( + name='Report', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])), + ('note', models.TextField(blank=True, null=True)), + ('resolved', models.BooleanField(default=False)), + ('reporter', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reporter', to=settings.AUTH_USER_MODEL)), + ('statuses', models.ManyToManyField(to='bookwyrm.Status')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ReportComment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])), + ('note', models.TextField()), + ('report', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Report')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 67ee16d3..326a673e 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -21,6 +21,7 @@ from .tag import Tag, UserTag from .user import User, KeyPair, AnnualGoal from .relationship import UserFollows, UserFollowRequest, UserBlocks +from .report import Report, ReportComment from .federated_server import FederatedServer from .import_job import ImportJob, ImportItem diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index ce804310..8f7d903e 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -4,7 +4,7 @@ from .base_model import BookWyrmModel class FederatedServer(BookWyrmModel): - """ store which server's we federate with """ + """ store which servers we federate with """ server_name = models.CharField(max_length=255, unique=True) # federated, blocked, whatever else diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py new file mode 100644 index 00000000..afbc356b --- /dev/null +++ b/bookwyrm/models/report.py @@ -0,0 +1,23 @@ +""" flagged for moderation """ +from django.db import models +from .base_model import BookWyrmModel + + +class Report(BookWyrmModel): + """ reported status or user """ + + reporter = models.ForeignKey( + "User", related_name="reporter", on_delete=models.PROTECT + ) + note = models.TextField(null=True, blank=True) + user = models.ForeignKey("User", on_delete=models.PROTECT) + statuses = models.ManyToManyField("Status") + resolved = models.BooleanField(default=False) + + +class ReportComment(BookWyrmModel): + """ updates on a report """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + note = models.TextField() + report = models.ForeignKey(Report, on_delete=models.PROTECT) diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index 3b8dec26..d35633cb 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/test_reports.py @@ -20,6 +20,13 @@ class ReportViews(TestCase): local=True, localname="mouse", ) + self.local_user = models.User.objects.create_user( + "rat@local.com", + "rat@mouse.mouse", + "password", + local=True, + localname="rat", + ) models.SiteSettings.objects.create() def test_reports_page(self): @@ -39,7 +46,7 @@ class ReportViews(TestCase): request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True - report = models.Report.objects.create() + report = models.Report.objects.create(reporter=self.local_user, user=self.rat) result = view(request, report.id) diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index a90fffaf..2e374d5e 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -8,7 +8,7 @@ from django.views import View from bookwyrm import models -# pylint: disable= no-self-use +# pylint: disable=no-self-use @method_decorator(login_required, name="dispatch") @method_decorator( permission_required("bookwyrm.moderate_user", raise_exception=True), @@ -21,11 +21,13 @@ from bookwyrm import models class Reports(View): """ list of reports """ - def get(self, request, status="open"): + def get(self, request): """ view current reports """ + resolved = request.GET.get("resolved") data = { - "status": status - } # {"reports": models.Report.objects.filter(status=status)} + "resolved": resolved, + "reports": models.Report.objects.filter(resolved=resolved), + } return TemplateResponse(request, "settings/reports.html", data) @@ -34,5 +36,5 @@ class Report(View): def get(self, request, report_id): """ load a report """ - data = {"report": get_object_or_404(models.REport, id=report_id)} + data = {"report": get_object_or_404(models.Report, id=report_id)} return TemplateResponse(request, "settings/report.html", data) From 21f199c5482d2aae87058433ce68c2d6930e610a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 18:36:34 -0800 Subject: [PATCH 18/54] Make reports --- bookwyrm/forms.py | 6 ++++ ...tcomment.py => 0049_auto_20210309_0156.py} | 12 ++++--- bookwyrm/models/report.py | 11 ++++++- bookwyrm/templates/settings/report.html | 2 ++ .../templates/settings/report_preview.html | 4 +-- .../templates/snippets/report_button.html | 7 ++++ .../snippets/status/status_options.html | 2 +- bookwyrm/templates/snippets/user_options.html | 2 +- bookwyrm/tests/views/test_reports.py | 33 +++++++++++++++++-- bookwyrm/urls.py | 5 +-- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/reports.py | 29 ++++++++++++++-- 12 files changed, 96 insertions(+), 19 deletions(-) rename bookwyrm/migrations/{0049_report_reportcomment.py => 0049_auto_20210309_0156.py} (83%) create mode 100644 bookwyrm/templates/settings/report.html create mode 100644 bookwyrm/templates/snippets/report_button.html diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 380e701f..654130cf 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -231,3 +231,9 @@ class ListForm(CustomForm): class Meta: model = models.List fields = ["user", "name", "description", "curation", "privacy"] + + +class ReportForm(CustomForm): + class Meta: + model = models.Report + fields = ["user", "reporter", "statuses", "note"] diff --git a/bookwyrm/migrations/0049_report_reportcomment.py b/bookwyrm/migrations/0049_auto_20210309_0156.py similarity index 83% rename from bookwyrm/migrations/0049_report_reportcomment.py rename to bookwyrm/migrations/0049_auto_20210309_0156.py index deb8ba6f..494f5bc8 100644 --- a/bookwyrm/migrations/0049_report_reportcomment.py +++ b/bookwyrm/migrations/0049_auto_20210309_0156.py @@ -1,9 +1,10 @@ -# Generated by Django 3.0.7 on 2021-03-09 00:55 +# Generated by Django 3.0.7 on 2021-03-09 01:56 import bookwyrm.models.fields from django.conf import settings from django.db import migrations, models import django.db.models.deletion +import django.db.models.expressions class Migration(migrations.Migration): @@ -23,12 +24,9 @@ class Migration(migrations.Migration): ('note', models.TextField(blank=True, null=True)), ('resolved', models.BooleanField(default=False)), ('reporter', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reporter', to=settings.AUTH_USER_MODEL)), - ('statuses', models.ManyToManyField(to='bookwyrm.Status')), + ('statuses', models.ManyToManyField(blank=True, null=True, to='bookwyrm.Status')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), ], - options={ - 'abstract': False, - }, ), migrations.CreateModel( name='ReportComment', @@ -45,4 +43,8 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.AddConstraint( + model_name='report', + constraint=models.CheckConstraint(check=models.Q(_negated=True, reporter=django.db.models.expressions.F('user')), name='self_report'), + ), ] diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py index afbc356b..e1e8c2a4 100644 --- a/bookwyrm/models/report.py +++ b/bookwyrm/models/report.py @@ -1,5 +1,6 @@ """ flagged for moderation """ from django.db import models +from django.db.models import F, Q from .base_model import BookWyrmModel @@ -11,9 +12,17 @@ class Report(BookWyrmModel): ) note = models.TextField(null=True, blank=True) user = models.ForeignKey("User", on_delete=models.PROTECT) - statuses = models.ManyToManyField("Status") + statuses = models.ManyToManyField("Status", null=True, blank=True) resolved = models.BooleanField(default=False) + class Meta: + """ don't let users report themselves """ + constraints = [ + models.CheckConstraint( + check=~Q(reporter=F('user')), + name='self_report' + ) + ] class ReportComment(BookWyrmModel): """ updates on a report """ diff --git a/bookwyrm/templates/settings/report.html b/bookwyrm/templates/settings/report.html new file mode 100644 index 00000000..1f55906b --- /dev/null +++ b/bookwyrm/templates/settings/report.html @@ -0,0 +1,2 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} diff --git a/bookwyrm/templates/settings/report_preview.html b/bookwyrm/templates/settings/report_preview.html index 67bffe27..b72dd958 100644 --- a/bookwyrm/templates/settings/report_preview.html +++ b/bookwyrm/templates/settings/report_preview.html @@ -2,7 +2,7 @@ {% load i18n %} {% block card-header %}

    - report title

    + report title {% endblock %} {% block card-content %} @@ -11,4 +11,4 @@ about this report {% block card-footer %} footer -{% endblock +{% endblock %} diff --git a/bookwyrm/templates/snippets/report_button.html b/bookwyrm/templates/snippets/report_button.html new file mode 100644 index 00000000..5fbaee99 --- /dev/null +++ b/bookwyrm/templates/snippets/report_button.html @@ -0,0 +1,7 @@ +{% load i18n %} +
    + {% csrf_token %} + + + +
    diff --git a/bookwyrm/templates/snippets/status/status_options.html b/bookwyrm/templates/snippets/status/status_options.html index 0f099e17..f703ba6e 100644 --- a/bookwyrm/templates/snippets/status/status_options.html +++ b/bookwyrm/templates/snippets/status/status_options.html @@ -25,7 +25,7 @@ {% trans "Send direct message" %}
  • - {% trans "Report status" %} + {% include 'snippets/report_button.html' with user=status.user class="is-fullwidth" %}
  • {% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %} diff --git a/bookwyrm/templates/snippets/user_options.html b/bookwyrm/templates/snippets/user_options.html index c9fdac0a..585417c7 100644 --- a/bookwyrm/templates/snippets/user_options.html +++ b/bookwyrm/templates/snippets/user_options.html @@ -13,7 +13,7 @@ {% trans "Send direct message" %}
  • - {% trans "Report user" %} + {% include 'snippets/report_button.html' with user=status.user class="is-fullwidth" %}
  • {% include 'snippets/block_button.html' with user=user class="is-fullwidth" %} diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index d35633cb..70414dcb 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/test_reports.py @@ -3,8 +3,7 @@ from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import models -from bookwyrm import views +from bookwyrm import forms, models, views class ReportViews(TestCase): @@ -20,7 +19,7 @@ class ReportViews(TestCase): local=True, localname="mouse", ) - self.local_user = models.User.objects.create_user( + self.rat = models.User.objects.create_user( "rat@local.com", "rat@mouse.mouse", "password", @@ -35,6 +34,20 @@ class ReportViews(TestCase): request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True + + result = view(request) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + def test_reports_page_with_data(self): + """ there are so many views, this just makes sure it LOADS """ + view = views.Reports.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + report = models.Report.objects.create(reporter=self.local_user, user=self.rat) + result = view(request) self.assertIsInstance(result, TemplateResponse) result.render() @@ -53,3 +66,17 @@ class ReportViews(TestCase): self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) + + def test_make_report(self): + """ a user reports another user """ + form = forms.ReportForm() + form.data["reporter"] = self.local_user.id + form.data["user"] = self.rat.id + request = self.factory.post("", form.data) + request.user = self.local_user + + views.make_report(request) + + report = models.Report.objects.get() + self.assertEqual(report.reporter, self.local_user) + self.assertEqual(report.user, self.rat) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index c7ec7f4b..42b9803d 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -54,14 +54,15 @@ urlpatterns = [ re_path( r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites" ), + re_path(r"^invite/(?P[A-Za-z0-9]+)/?$", views.Invite.as_view()), # moderation re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"), re_path( - r"^settings/report/(?P\d+)/?$", + r"^settings/reports/(?P\d+)/?$", views.Report.as_view(), name="settings-report", ), - re_path(r"^invite/(?P[A-Za-z0-9]+)/?$", views.Invite.as_view()), + re_path(r"^report/?$", views.make_report, name="report"), # landing pages re_path(r"^about/?$", views.About.as_view()), path("", views.Home.as_view()), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 36a64ddf..b433dca2 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -20,7 +20,7 @@ from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import start_reading, finish_reading, delete_progressupdate -from .reports import Report, Reports +from .reports import Report, Reports, make_report from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword from .search import Search diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index 2e374d5e..9eaf9bdc 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -1,11 +1,12 @@ """ moderation via flagged posts and users """ from django.contrib.auth.decorators import login_required, permission_required -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View +from django.views.decorators.http import require_POST -from bookwyrm import models +from bookwyrm import forms, models # pylint: disable=no-self-use @@ -23,7 +24,7 @@ class Reports(View): def get(self, request): """ view current reports """ - resolved = request.GET.get("resolved") + resolved = request.GET.get("resolved", False) data = { "resolved": resolved, "reports": models.Report.objects.filter(resolved=resolved), @@ -31,6 +32,15 @@ class Reports(View): return TemplateResponse(request, "settings/reports.html", data) +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("bookwyrm.moderate_user", raise_exception=True), + name="dispatch", +) +@method_decorator( + permission_required("bookwyrm.moderate_post", raise_exception=True), + name="dispatch", +) class Report(View): """ view a specific report """ @@ -38,3 +48,16 @@ class Report(View): """ load a report """ data = {"report": get_object_or_404(models.Report, id=report_id)} return TemplateResponse(request, "settings/report.html", data) + + +@login_required +@require_POST +def make_report(request): + """ a user reports something """ + form = forms.ReportForm(request.POST) + if not form.is_valid(): + print(form.errors) + return redirect(request.headers.get("Referer", "/")) + + form.save() + return redirect(request.headers.get("Referer", "/")) From ff624e33fa93a46ef238596709eea4b96d63bb14 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 9 Mar 2021 10:08:02 -0800 Subject: [PATCH 19/54] fixes display of report items on admin page --- bookwyrm/templates/settings/admin_layout.html | 2 +- bookwyrm/templates/settings/report_preview.html | 13 +++++++++---- bookwyrm/templates/settings/reports.html | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/bookwyrm/templates/settings/admin_layout.html b/bookwyrm/templates/settings/admin_layout.html index 312d502a..a34fe638 100644 --- a/bookwyrm/templates/settings/admin_layout.html +++ b/bookwyrm/templates/settings/admin_layout.html @@ -46,7 +46,7 @@ {% endif %} -
    +
    {% block panel %}{% endblock %}
    diff --git a/bookwyrm/templates/settings/report_preview.html b/bookwyrm/templates/settings/report_preview.html index b72dd958..63eca0fc 100644 --- a/bookwyrm/templates/settings/report_preview.html +++ b/bookwyrm/templates/settings/report_preview.html @@ -1,14 +1,19 @@ {% extends 'components/card.html' %} {% load i18n %} {% block card-header %} -

    - report title

    +

    + report title +

    {% endblock %} {% block card-content %} -about this report +
    + about this report +
    {% endblock %} {% block card-footer %} -footer + {% endblock %} diff --git a/bookwyrm/templates/settings/reports.html b/bookwyrm/templates/settings/reports.html index efca7244..59fbaf56 100644 --- a/bookwyrm/templates/settings/reports.html +++ b/bookwyrm/templates/settings/reports.html @@ -15,13 +15,13 @@
  • -
      +
      {% for report in reports %} -
    • +
      {% include 'settings/report_preview.html' with report=report %} -
    • +
      {% endfor %} -
    + {% endblock %} From 999bff4bbadb25f0e1a3015a4ece75bcc0559baa Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 9 Mar 2021 12:35:07 -0800 Subject: [PATCH 20/54] Basic reports admin templates --- bookwyrm/templates/settings/report.html | 41 +++++++++++++++++++ .../templates/settings/report_preview.html | 13 ++++-- bookwyrm/templates/settings/reports.html | 9 ++-- bookwyrm/views/reports.py | 2 +- 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/bookwyrm/templates/settings/report.html b/bookwyrm/templates/settings/report.html index 1f55906b..74c3641b 100644 --- a/bookwyrm/templates/settings/report.html +++ b/bookwyrm/templates/settings/report.html @@ -1,2 +1,43 @@ {% extends 'settings/admin_layout.html' %} {% load i18n %} + +{% block title %}{% blocktrans with report_id=report.id %}Report #{{ report_id }}{% endblocktrans %}{% endblock %} +{% block header %}{% blocktrans with report_id=report.id %}Report #{{ report_id }}{% endblocktrans %}{% endblock %} + +{% block panel %} + + +
    + {% include 'settings/report_preview.html' with report=report %} +
    + +
    +

    {% trans "Actions" %}

    +
    + + +
    + + {% for comment in report.reportcomment_set.all %} +
    + {{ comment }} +
    + {% endfor %} +
    + + + + +
    + +
    +

    {% trans "Reported statuses" %}

    +
      + {% for status in report.statuses.all %} +
    • {{ status.id }}
    • + {% endfor %} +
    +
    +{% endblock %} diff --git a/bookwyrm/templates/settings/report_preview.html b/bookwyrm/templates/settings/report_preview.html index 63eca0fc..25cb8a2f 100644 --- a/bookwyrm/templates/settings/report_preview.html +++ b/bookwyrm/templates/settings/report_preview.html @@ -1,19 +1,26 @@ {% extends 'components/card.html' %} {% load i18n %} +{% load humanize %} {% block card-header %}

    - report title + {{ report.user.username }}

    {% endblock %} {% block card-content %}
    - about this report + {% if report.notes %}{{ report.notes }}{% else %}{% trans "No notes provided" %}{% endif %}
    {% endblock %} {% block card-footer %} + + {% endblock %} diff --git a/bookwyrm/templates/settings/reports.html b/bookwyrm/templates/settings/reports.html index 59fbaf56..329901ea 100644 --- a/bookwyrm/templates/settings/reports.html +++ b/bookwyrm/templates/settings/reports.html @@ -1,16 +1,17 @@ {% extends 'settings/admin_layout.html' %} {% load i18n %} +{% block title %}{% trans "Reports" %}{% endblock %} {% block header %}{% trans "Reports" %}{% endblock %} {% block panel %} diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index 9eaf9bdc..eda56532 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -24,7 +24,7 @@ class Reports(View): def get(self, request): """ view current reports """ - resolved = request.GET.get("resolved", False) + resolved = request.GET.get("resolved") == "true" data = { "resolved": resolved, "reports": models.Report.objects.filter(resolved=resolved), From 0d2c641d01dc7017ebb2a45d8941569b1d5e3df5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 9 Mar 2021 12:57:38 -0800 Subject: [PATCH 21/54] Reformats report model --- .../migrations/0049_auto_20210309_0156.py | 107 ++++++++++++++---- bookwyrm/models/report.py | 7 +- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/bookwyrm/migrations/0049_auto_20210309_0156.py b/bookwyrm/migrations/0049_auto_20210309_0156.py index 494f5bc8..ae9d77a8 100644 --- a/bookwyrm/migrations/0049_auto_20210309_0156.py +++ b/bookwyrm/migrations/0049_auto_20210309_0156.py @@ -10,41 +10,104 @@ import django.db.models.expressions class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0048_merge_20210308_1754'), + ("bookwyrm", "0048_merge_20210308_1754"), ] operations = [ migrations.CreateModel( - name='Report', + name="Report", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])), - ('note', models.TextField(blank=True, null=True)), - ('resolved', models.BooleanField(default=False)), - ('reporter', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reporter', to=settings.AUTH_USER_MODEL)), - ('statuses', models.ManyToManyField(blank=True, null=True, to='bookwyrm.Status')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "remote_id", + bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + ("note", models.TextField(blank=True, null=True)), + ("resolved", models.BooleanField(default=False)), + ( + "reporter", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reporter", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "statuses", + models.ManyToManyField(blank=True, null=True, to="bookwyrm.Status"), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='ReportComment', + name="ReportComment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])), - ('note', models.TextField()), - ('report', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Report')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "remote_id", + bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + ("note", models.TextField()), + ( + "report", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Report", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.AddConstraint( - model_name='report', - constraint=models.CheckConstraint(check=models.Q(_negated=True, reporter=django.db.models.expressions.F('user')), name='self_report'), + model_name="report", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, reporter=django.db.models.expressions.F("user") + ), + name="self_report", + ), ), ] diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py index e1e8c2a4..3a8fdd13 100644 --- a/bookwyrm/models/report.py +++ b/bookwyrm/models/report.py @@ -17,13 +17,12 @@ class Report(BookWyrmModel): class Meta: """ don't let users report themselves """ + constraints = [ - models.CheckConstraint( - check=~Q(reporter=F('user')), - name='self_report' - ) + models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report") ] + class ReportComment(BookWyrmModel): """ updates on a report """ From 7f452066939495ee74a7f2dac129bae1c1e2d7e8 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 10 Mar 2021 12:38:49 -0800 Subject: [PATCH 22/54] Moves moderation templates to their own directory --- bookwyrm/templates/{settings => moderation}/report.html | 2 +- .../templates/{settings => moderation}/report_preview.html | 0 bookwyrm/templates/{settings => moderation}/reports.html | 2 +- bookwyrm/templates/snippets/report_button.html | 1 + bookwyrm/views/reports.py | 4 ++-- 5 files changed, 5 insertions(+), 4 deletions(-) rename bookwyrm/templates/{settings => moderation}/report.html (94%) rename bookwyrm/templates/{settings => moderation}/report_preview.html (100%) rename bookwyrm/templates/{settings => moderation}/reports.html (91%) diff --git a/bookwyrm/templates/settings/report.html b/bookwyrm/templates/moderation/report.html similarity index 94% rename from bookwyrm/templates/settings/report.html rename to bookwyrm/templates/moderation/report.html index 74c3641b..ca68d51d 100644 --- a/bookwyrm/templates/settings/report.html +++ b/bookwyrm/templates/moderation/report.html @@ -10,7 +10,7 @@
    - {% include 'settings/report_preview.html' with report=report %} + {% include 'moderation/report_preview.html' with report=report %}
    diff --git a/bookwyrm/templates/settings/report_preview.html b/bookwyrm/templates/moderation/report_preview.html similarity index 100% rename from bookwyrm/templates/settings/report_preview.html rename to bookwyrm/templates/moderation/report_preview.html diff --git a/bookwyrm/templates/settings/reports.html b/bookwyrm/templates/moderation/reports.html similarity index 91% rename from bookwyrm/templates/settings/reports.html rename to bookwyrm/templates/moderation/reports.html index 329901ea..ebf29a7a 100644 --- a/bookwyrm/templates/settings/reports.html +++ b/bookwyrm/templates/moderation/reports.html @@ -19,7 +19,7 @@
    {% for report in reports %}
    - {% include 'settings/report_preview.html' with report=report %} + {% include 'moderation/report_preview.html' with report=report %}
    {% endfor %}
    diff --git a/bookwyrm/templates/snippets/report_button.html b/bookwyrm/templates/snippets/report_button.html index 5fbaee99..9d32d5fb 100644 --- a/bookwyrm/templates/snippets/report_button.html +++ b/bookwyrm/templates/snippets/report_button.html @@ -3,5 +3,6 @@ {% csrf_token %} + diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index eda56532..87547081 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -29,7 +29,7 @@ class Reports(View): "resolved": resolved, "reports": models.Report.objects.filter(resolved=resolved), } - return TemplateResponse(request, "settings/reports.html", data) + return TemplateResponse(request, "moderation/reports.html", data) @method_decorator(login_required, name="dispatch") @@ -47,7 +47,7 @@ class Report(View): def get(self, request, report_id): """ load a report """ data = {"report": get_object_or_404(models.Report, id=report_id)} - return TemplateResponse(request, "settings/report.html", data) + return TemplateResponse(request, "moderation/report.html", data) @login_required From 965d84f86f4e9bef3eabeac6512058183229c462 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 11 Mar 2021 15:41:12 -0800 Subject: [PATCH 23/54] Fixes creating news works --- bookwyrm/templates/edit_book.html | 2 +- bookwyrm/views/books.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index 700dc570..e42f77f2 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -60,7 +60,7 @@ {% for match in book_matches %} {% endfor %} - + {% endif %}
    diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index ae60c677..9048f43d 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -203,7 +203,7 @@ class ConfirmEditBook(View): # create work, if needed if not book_id: work_match = request.POST.get("parent_work") - if work_match: + if work_match and work_match != "0": work = get_object_or_404(models.Work, id=work_match) else: work = models.Work.objects.create(title=form.cleaned_data["title"]) From c1976dbd62bf5491b4e5036e2005ddd1ba61db12 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 11 Mar 2021 16:33:49 -0800 Subject: [PATCH 24/54] Add multiple authors --- bookwyrm/templates/edit_book.html | 55 ++++++++++++++++++------------- bookwyrm/views/books.py | 47 +++++++++++++++----------- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index e42f77f2..107e75c5 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -39,29 +39,37 @@

    {% trans "Confirm Book Info" %}

    - {% if author_matches.exists %} -
    - {% blocktrans with name=add_author %}Is "{{ name }}" an existing author?{% endblocktrans %} - {% for match in author_matches %} - -

    - {% blocktrans with book_title=match.book_set.first.title %}Author of {{ book_title }}{% endblocktrans %} -

    + {% if author_matches %} +
    + {% for author in author_matches %} +
    + {% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %} + {% with forloop.counter as counter %} + {% for match in author.matches %} + +

    + {% blocktrans with book_title=match.book_set.first.title %}Author of {{ book_title }}{% endblocktrans %} +

    + {% endfor %} + + {% endwith %} +
    {% endfor %} - -
    +
    {% else %}

    {% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}

    {% endif %} {% if not book %} -
    - {% trans "Is this an editions of an existing work?" %} - {% for match in book_matches %} - - {% endfor %} - -
    +
    +
    + {% trans "Is this an edition of an existing work?" %} + {% for match in book_matches %} + + {% endfor %} + +
    +
    {% endif %}
    @@ -109,16 +117,19 @@

    {% trans "Authors" %}

    + {% if book.authors.exists %}
    {% for author in book.authors.all %} -

    {{ author.name }} -

    - - + {% endif %} + +

    Separate multiple author names with commas.

    +
    diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 9048f43d..ff0d6764 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -131,17 +131,22 @@ class EditBook(View): # we're adding an author through a free text field if add_author: data["add_author"] = add_author - # check for existing authors - vector = SearchVector("name", weight="A") + SearchVector( - "aliases", weight="B" - ) + data['author_matches'] = [] + for author in add_author.split(','): + # check for existing authors + vector = SearchVector("name", weight="A") + SearchVector( + "aliases", weight="B" + ) - data["author_matches"] = ( - models.Author.objects.annotate(search=vector) - .annotate(rank=SearchRank(vector, add_author)) - .filter(rank__gt=0.4) - .order_by("-rank")[:5] - ) + data["author_matches"].append({ + 'name': author.strip(), + 'matches': ( + models.Author.objects.annotate(search=vector) + .annotate(rank=SearchRank(vector, add_author)) + .filter(rank__gt=0.4) + .order_by("-rank")[:5] + ) + }) # we're creating a new book if not book: @@ -157,6 +162,8 @@ class EditBook(View): if add_author or not book: # creting a book or adding an author to a book needs another step data["confirm_mode"] = True + # this isn't preserved because it isn't part of the form obj + data["remove_authors"] = request.POST.getlist("remove_authors") return TemplateResponse(request, "edit_book.html", data) remove_authors = request.POST.getlist("remove_authors") @@ -190,15 +197,17 @@ class ConfirmEditBook(View): # get or create author as needed if request.POST.get("add_author"): - if request.POST.get("author_match"): - author = get_object_or_404( - models.Author, id=request.POST["author_match"] - ) - else: - author = models.Author.objects.create( - name=request.POST.get("add_author") - ) - book.authors.add(author) + for (i, author) in enumerate(request.POST.get("add_author").split(',')): + match = request.POST.get("author_match-%d" % i) + if match and match != "0": + author = get_object_or_404( + models.Author, id=request.POST["author_match-%d" % i] + ) + else: + author = models.Author.objects.create( + name=author.strip() + ) + book.authors.add(author) # create work, if needed if not book_id: From 28db3e2733d57e9b7a5988e22d2e6c376587496a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 11 Mar 2021 16:40:35 -0800 Subject: [PATCH 25/54] Formatting --- bookwyrm/views/books.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index ff0d6764..1cb21f5e 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -131,22 +131,24 @@ class EditBook(View): # we're adding an author through a free text field if add_author: data["add_author"] = add_author - data['author_matches'] = [] - for author in add_author.split(','): + data["author_matches"] = [] + for author in add_author.split(","): # check for existing authors vector = SearchVector("name", weight="A") + SearchVector( "aliases", weight="B" ) - data["author_matches"].append({ - 'name': author.strip(), - 'matches': ( - models.Author.objects.annotate(search=vector) - .annotate(rank=SearchRank(vector, add_author)) - .filter(rank__gt=0.4) - .order_by("-rank")[:5] - ) - }) + data["author_matches"].append( + { + "name": author.strip(), + "matches": ( + models.Author.objects.annotate(search=vector) + .annotate(rank=SearchRank(vector, add_author)) + .filter(rank__gt=0.4) + .order_by("-rank")[:5] + ), + } + ) # we're creating a new book if not book: @@ -197,16 +199,14 @@ class ConfirmEditBook(View): # get or create author as needed if request.POST.get("add_author"): - for (i, author) in enumerate(request.POST.get("add_author").split(',')): + for (i, author) in enumerate(request.POST.get("add_author").split(",")): match = request.POST.get("author_match-%d" % i) if match and match != "0": author = get_object_or_404( models.Author, id=request.POST["author_match-%d" % i] ) else: - author = models.Author.objects.create( - name=author.strip() - ) + author = models.Author.objects.create(name=author.strip()) book.authors.add(author) # create work, if needed From 33b8537a3d692c2dcbcab73b7c4da11958c79094 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 11 Mar 2021 17:38:21 -0800 Subject: [PATCH 26/54] Let user supply a note for report --- bookwyrm/models/report.py | 1 + bookwyrm/templates/moderation/report.html | 10 +++-- .../templates/moderation/report_modal.html | 37 +++++++++++++++++++ .../templates/moderation/report_preview.html | 2 +- bookwyrm/templates/notifications.html | 10 +---- .../templates/snippets/report_button.html | 16 ++++---- .../snippets/status/status_body.html | 9 ++++- .../snippets/status/status_options.html | 4 +- .../templates/snippets/status_preview.html | 9 +++++ 9 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 bookwyrm/templates/moderation/report_modal.html create mode 100644 bookwyrm/templates/snippets/status_preview.html diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py index 3a8fdd13..8893f42f 100644 --- a/bookwyrm/models/report.py +++ b/bookwyrm/models/report.py @@ -21,6 +21,7 @@ class Report(BookWyrmModel): constraints = [ models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report") ] + ordering = ("-created_date",) class ReportComment(BookWyrmModel): diff --git a/bookwyrm/templates/moderation/report.html b/bookwyrm/templates/moderation/report.html index ca68d51d..ce0a0b3a 100644 --- a/bookwyrm/templates/moderation/report.html +++ b/bookwyrm/templates/moderation/report.html @@ -32,11 +32,13 @@ -
    -

    {% trans "Reported statuses" %}

    +
    +

    {% trans "Reported statuses" %}

      - {% for status in report.statuses.all %} -
    • {{ status.id }}
    • + {% for status in report.statuses.select_subclasses.all %} +
    • + {% include 'snippets/status/status.html' with status=status moderation_mode=True %} +
    • {% endfor %}
    diff --git a/bookwyrm/templates/moderation/report_modal.html b/bookwyrm/templates/moderation/report_modal.html new file mode 100644 index 00000000..28613130 --- /dev/null +++ b/bookwyrm/templates/moderation/report_modal.html @@ -0,0 +1,37 @@ +{% extends 'components/modal.html' %} +{% load i18n %} +{% load humanize %} + +{% block modal-title %} +{% blocktrans with username=user.username %}Report @{{ username }}{% endblocktrans %} +{% endblock %} + +{% block modal-form-open %} +
    +{% endblock %} + +{% block modal-body %} + +{% csrf_token %} + + + + +
    +

    {% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}

    + + +
    + +{% endblock %} + + +{% block modal-footer %} + + +{% trans "Cancel" as button_text %} +{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="report" controls_uid=report_uuid class="" %} + +{% endblock %} +{% block modal-form-close %}
    {% endblock %} + diff --git a/bookwyrm/templates/moderation/report_preview.html b/bookwyrm/templates/moderation/report_preview.html index 25cb8a2f..9acc4f77 100644 --- a/bookwyrm/templates/moderation/report_preview.html +++ b/bookwyrm/templates/moderation/report_preview.html @@ -9,7 +9,7 @@ {% block card-content %}
    - {% if report.notes %}{{ report.notes }}{% else %}{% trans "No notes provided" %}{% endif %} + {% if report.note %}{{ report.note }}{% else %}{% trans "No notes provided" %}{% endif %}
    {% endblock %} diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index 80ee2250..3f0300bd 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -115,15 +115,7 @@
    - {% if related_status.content %} - - {{ related_status.content | safe | truncatewords_html:10 }}{% if related_status.mention_books %} {{ related_status.mention_books.first.title }}{% endif %} - - {% elif related_status.quote %} - {{ related_status.quote | safe | truncatewords_html:10 }} - {% elif related_status.rating %} - {% include 'snippets/stars.html' with rating=related_status.rating %} - {% endif %} + {% include 'snippets/status_preview.html' with status=related_status %}
    {{ related_status.published_date | post_date }} diff --git a/bookwyrm/templates/snippets/report_button.html b/bookwyrm/templates/snippets/report_button.html index 9d32d5fb..2fa0a3f3 100644 --- a/bookwyrm/templates/snippets/report_button.html +++ b/bookwyrm/templates/snippets/report_button.html @@ -1,8 +1,10 @@ {% load i18n %} -
    - {% csrf_token %} - - - - -
    +{% load bookwyrm_tags %} +{% with 0|uuid as report_uuid %} + +{% trans "Report" as button_text %} +{% include 'snippets/toggle/toggle_button.html' with class="is-danger is-light is-small is-fullwidth" text=button_text controls_text="report" controls_uid=report_uuid focus="modal-title-report" disabled=is_current %} + +{% include 'moderation/report_modal.html' with user=user reporter=request.user controls_text="report" controls_uid=report_uuid %} + +{% endwith %} diff --git a/bookwyrm/templates/snippets/status/status_body.html b/bookwyrm/templates/snippets/status/status_body.html index 8d6c21ed..f3732d1a 100644 --- a/bookwyrm/templates/snippets/status/status_body.html +++ b/bookwyrm/templates/snippets/status/status_body.html @@ -18,7 +18,10 @@ {% block card-footer %} {% endblock %} diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index 70414dcb..1a5bfd15 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/test_reports.py @@ -46,7 +46,7 @@ class ReportViews(TestCase): request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True - report = models.Report.objects.create(reporter=self.local_user, user=self.rat) + models.Report.objects.create(reporter=self.local_user, user=self.rat) result = view(request) self.assertIsInstance(result, TemplateResponse) @@ -80,3 +80,21 @@ class ReportViews(TestCase): report = models.Report.objects.get() self.assertEqual(report.reporter, self.local_user) self.assertEqual(report.user, self.rat) + + def test_resolve_report(self): + """ toggle report resolution status """ + report = models.Report.objects.create(reporter=self.local_user, user=self.rat) + self.assertFalse(report.resolved) + request = self.factory.post("") + request.user = self.local_user + request.user.is_superuser = True + + # resolve + views.resolve_report(request, report.id) + report.refresh_from_db() + self.assertTrue(report.resolved) + + # un-resolve + views.resolve_report(request, report.id) + report.refresh_from_db() + self.assertFalse(report.resolved) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 42b9803d..551be1e3 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -62,6 +62,11 @@ urlpatterns = [ views.Report.as_view(), name="settings-report", ), + re_path( + r"^settings/reports/(?P\d+)/resolve/?$", + views.resolve_report, + name="settings-report-resolve", + ), re_path(r"^report/?$", views.make_report, name="report"), # landing pages re_path(r"^about/?$", views.About.as_view()), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index b433dca2..63ab98e5 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -20,7 +20,7 @@ from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import start_reading, finish_reading, delete_progressupdate -from .reports import Report, Reports, make_report +from .reports import Report, Reports, make_report, resolve_report from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword from .search import Search diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index 87547081..59b9b9e4 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -49,6 +49,21 @@ class Report(View): data = {"report": get_object_or_404(models.Report, id=report_id)} return TemplateResponse(request, "moderation/report.html", data) + def post(self, request, report_id): + """ update a report """ + + +@login_required +@permission_required("bookwyrm_moderate_post") +def resolve_report(_, report_id): + """ mark a report as (un)resolved """ + report = get_object_or_404(models.Report, id=report_id) + report.resolved = not report.resolved + report.save() + if not report.resolved: + return redirect("settings-report", report.id) + return redirect("settings-reports") + @login_required @require_POST From 8bd12f0e062337a5bfeb6b103f7d36ab8498ab61 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 12 Mar 2021 10:27:08 -0800 Subject: [PATCH 30/54] Remove unused method --- bookwyrm/views/reports.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index 59b9b9e4..ad22da2b 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -49,9 +49,6 @@ class Report(View): data = {"report": get_object_or_404(models.Report, id=report_id)} return TemplateResponse(request, "moderation/report.html", data) - def post(self, request, report_id): - """ update a report """ - @login_required @permission_required("bookwyrm_moderate_post") From 422cd2da73c7583a1383bb6f5722295711fd8eb5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 12 Mar 2021 10:37:52 -0800 Subject: [PATCH 31/54] Direct message report action --- bookwyrm/templates/moderation/report.html | 2 +- bookwyrm/templates/snippets/status/status_options.html | 2 +- bookwyrm/urls.py | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/moderation/report.html b/bookwyrm/templates/moderation/report.html index ce0a0b3a..32fdf6d7 100644 --- a/bookwyrm/templates/moderation/report.html +++ b/bookwyrm/templates/moderation/report.html @@ -16,7 +16,7 @@

    {% trans "Actions" %}

    - + {% trans "Send direct message" %}
    diff --git a/bookwyrm/templates/snippets/status/status_options.html b/bookwyrm/templates/snippets/status/status_options.html index 3c710055..6f0ca2e6 100644 --- a/bookwyrm/templates/snippets/status/status_options.html +++ b/bookwyrm/templates/snippets/status/status_options.html @@ -22,7 +22,7 @@ {% else %} {# things you can do to other people's statuses #}
  • - {% trans "Send direct message" %} + {% trans "Send direct message" %}
  • {% include 'snippets/report_button.html' with user=status.user status=status %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 551be1e3..0ad464f4 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -75,10 +75,13 @@ urlpatterns = [ re_path(r"^notifications/?$", views.Notifications.as_view()), # feeds re_path(r"^(?Phome|local|federated)/?$", views.Feed.as_view()), - re_path(r"^direct-messages/?$", views.DirectMessage.as_view()), + re_path( + r"^direct-messages/?$", views.DirectMessage.as_view(), name="direct-messages" + ), re_path( r"^direct-messages/(?P%s)?$" % regex.username, views.DirectMessage.as_view(), + name="direct-messages-user", ), # search re_path(r"^search/?$", views.Search.as_view()), From 677a49fee31cc2fe0e24848ea05cbe9a9f3ff63f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 12 Mar 2021 11:13:53 -0800 Subject: [PATCH 32/54] Option to deactivate reported users --- bookwyrm/templates/moderation/report.html | 15 ++++++++++++--- .../templates/moderation/report_preview.html | 7 +++++-- bookwyrm/tests/views/test_reports.py | 19 +++++++++++++++++++ bookwyrm/urls.py | 5 +++++ bookwyrm/views/__init__.py | 2 +- bookwyrm/views/reports.py | 10 ++++++++++ 6 files changed, 52 insertions(+), 6 deletions(-) diff --git a/bookwyrm/templates/moderation/report.html b/bookwyrm/templates/moderation/report.html index 32fdf6d7..b2e61cc4 100644 --- a/bookwyrm/templates/moderation/report.html +++ b/bookwyrm/templates/moderation/report.html @@ -15,9 +15,18 @@

    {% trans "Actions" %}

    -
    - {% trans "Send direct message" %} - +
    +

    + {% trans "Send direct message" %} +

    +
    + {% csrf_token %} + {% if report.user.is_active %} + + {% else %} + + {% endif %} +
    {% for comment in report.reportcomment_set.all %} diff --git a/bookwyrm/templates/moderation/report_preview.html b/bookwyrm/templates/moderation/report_preview.html index 3888be27..c35010cf 100644 --- a/bookwyrm/templates/moderation/report_preview.html +++ b/bookwyrm/templates/moderation/report_preview.html @@ -8,8 +8,11 @@ {% endblock %} {% block card-content %} -
    - {% if report.note %}{{ report.note }}{% else %}{% trans "No notes provided" %}{% endif %} +
    +

    + {% if report.note %}{{ report.note }}{% else %}{% trans "No notes provided" %}{% endif %} +

    +

    {% trans "View user profile" %}

    {% endblock %} diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index 1a5bfd15..724e7b5a 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/test_reports.py @@ -98,3 +98,22 @@ class ReportViews(TestCase): views.resolve_report(request, report.id) report.refresh_from_db() self.assertFalse(report.resolved) + + + def test_deactivate_user(self): + """ toggle whether a user is able to log in """ + self.assertTrue(self.rat.is_active) + report = models.Report.objects.create(reporter=self.local_user, user=self.rat) + request = self.factory.post("") + request.user = self.local_user + request.user.is_superuser = True + + # resolve + views.deactivate_user(request, report.id) + self.rat.refresh_from_db() + self.assertFalse(self.rat.is_active) + + # un-resolve + views.deactivate_user(request, report.id) + self.rat.refresh_from_db() + self.assertTrue(self.rat.is_active) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 0ad464f4..26ce67a3 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -62,6 +62,11 @@ urlpatterns = [ views.Report.as_view(), name="settings-report", ), + re_path( + r"^settings/reports/(?P\d+)/deactivate/?$", + views.deactivate_user, + name="settings-report-deactivate", + ), re_path( r"^settings/reports/(?P\d+)/resolve/?$", views.resolve_report, diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 63ab98e5..606624b8 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -20,7 +20,7 @@ from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import start_reading, finish_reading, delete_progressupdate -from .reports import Report, Reports, make_report, resolve_report +from .reports import Report, Reports, make_report, resolve_report, deactivate_user from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword from .search import Search diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index ad22da2b..2ba0d27a 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -50,6 +50,16 @@ class Report(View): return TemplateResponse(request, "moderation/report.html", data) +@login_required +@permission_required("bookwyrm_moderate_user") +def deactivate_user(_, report_id): + """ mark an account as inactive """ + report = get_object_or_404(models.Report, id=report_id) + report.user.is_active = not report.user.is_active + report.user.save() + return redirect("settings-report", report.id) + + @login_required @permission_required("bookwyrm_moderate_post") def resolve_report(_, report_id): From 8c74beb78cec60bf44723661b2b6dd2ed5304a3a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 12 Mar 2021 11:25:56 -0800 Subject: [PATCH 33/54] Allow moderators to delete reported statuses --- bookwyrm/templates/moderation/report.html | 5 +++ .../templates/moderation/report_preview.html | 1 - .../snippets/status/status_body.html | 9 ++++- bookwyrm/tests/views/test_reports.py | 1 - bookwyrm/tests/views/test_status.py | 34 ++++++++++++++++++- bookwyrm/views/status.py | 2 +- 6 files changed, 47 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/moderation/report.html b/bookwyrm/templates/moderation/report.html index b2e61cc4..ae014d68 100644 --- a/bookwyrm/templates/moderation/report.html +++ b/bookwyrm/templates/moderation/report.html @@ -15,6 +15,7 @@

    {% trans "Actions" %}

    +

    {% trans "View user profile" %}

    {% trans "Send direct message" %} @@ -46,7 +47,11 @@

      {% for status in report.statuses.select_subclasses.all %}
    • + {% if status.deleted %} + {% trans "Statuses has been deleted" %} + {% else %} {% include 'snippets/status/status.html' with status=status moderation_mode=True %} + {% endif %}
    • {% endfor %}
    diff --git a/bookwyrm/templates/moderation/report_preview.html b/bookwyrm/templates/moderation/report_preview.html index c35010cf..3a5ebcf9 100644 --- a/bookwyrm/templates/moderation/report_preview.html +++ b/bookwyrm/templates/moderation/report_preview.html @@ -12,7 +12,6 @@

    {% if report.note %}{{ report.note }}{% else %}{% trans "No notes provided" %}{% endif %}

    -

    {% trans "View user profile" %}

    {% endblock %} diff --git a/bookwyrm/templates/snippets/status/status_body.html b/bookwyrm/templates/snippets/status/status_body.html index f3732d1a..a7e8e884 100644 --- a/bookwyrm/templates/snippets/status/status_body.html +++ b/bookwyrm/templates/snippets/status/status_body.html @@ -19,8 +19,15 @@ {% block card-footer %}