{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}
- - {% include 'snippets/goal_form.html' %} -diff --git a/Dockerfile b/Dockerfile index 1892ae23..349dd82b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,4 @@ WORKDIR /app COPY requirements.txt /app/ RUN pip install -r requirements.txt --no-cache-dir -RUN apt-get update && apt-get install -y gettext libgettextpo-dev && apt-get clean +RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 23063ff7..5acde9ea 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -29,8 +29,7 @@ class CustomForm(ModelForm): input_type = visible.field.widget.input_type if isinstance(visible.field.widget, Textarea): input_type = "textarea" - visible.field.widget.attrs["cols"] = None - visible.field.widget.attrs["rows"] = None + visible.field.widget.attrs["rows"] = 5 visible.field.widget.attrs["class"] = css_classes[input_type] @@ -228,7 +227,7 @@ class ExpiryWidget(widgets.Select): elif selected_string == "forever": return None else: - return selected_string # "This will raise + return selected_string # This will raise return timezone.now() + interval @@ -269,7 +268,7 @@ class CreateInviteForm(CustomForm): class ShelfForm(CustomForm): class Meta: model = models.Shelf - fields = ["user", "name", "privacy"] + fields = ["user", "name", "privacy", "description"] class GoalForm(CustomForm): diff --git a/bookwyrm/migrations/0099_readthrough_is_active.py b/bookwyrm/migrations/0099_readthrough_is_active.py new file mode 100644 index 00000000..e7b177ba --- /dev/null +++ b/bookwyrm/migrations/0099_readthrough_is_active.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.4 on 2021-09-22 16:53 + +from django.db import migrations, models + + +def set_active_readthrough(apps, schema_editor): + """best-guess for deactivation date""" + db_alias = schema_editor.connection.alias + apps.get_model("bookwyrm", "ReadThrough").objects.using(db_alias).filter( + start_date__isnull=False, + finish_date__isnull=True, + ).update(is_active=True) + + +def reverse_func(apps, schema_editor): + """noop""" + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0098_auto_20210918_2238"), + ] + + operations = [ + migrations.AddField( + model_name="readthrough", + name="is_active", + field=models.BooleanField(default=False), + ), + migrations.RunPython(set_active_readthrough, reverse_func), + migrations.AlterField( + model_name="readthrough", + name="is_active", + field=models.BooleanField(default=True), + ), + ] diff --git a/bookwyrm/migrations/0100_shelf_description.py b/bookwyrm/migrations/0100_shelf_description.py new file mode 100644 index 00000000..18185b17 --- /dev/null +++ b/bookwyrm/migrations/0100_shelf_description.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2021-09-28 23:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0099_readthrough_is_active"), + ] + + operations = [ + migrations.AddField( + model_name="shelf", + name="description", + field=models.TextField(blank=True, max_length=500, null=True), + ), + ] diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index aa174a14..ca32521a 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -1,8 +1,11 @@ """ base model with default fields """ import base64 from Crypto import Random + +from django.core.exceptions import PermissionDenied from django.db import models from django.dispatch import receiver +from django.http import Http404 from django.utils.translation import gettext_lazy as _ from bookwyrm.settings import DOMAIN @@ -48,26 +51,26 @@ class BookWyrmModel(models.Model): """how to link to this object in the local app""" return self.get_remote_id().replace(f"https://{DOMAIN}", "") - def visible_to_user(self, viewer): + def raise_visible_to_user(self, viewer): """is a user authorized to view an object?""" # make sure this is an object with privacy owned by a user if not hasattr(self, "user") or not hasattr(self, "privacy"): - return None + return # viewer can't see it if the object's owner blocked them if viewer in self.user.blocks.all(): - return False + raise Http404() # you can see your own posts and any public or unlisted posts if viewer == self.user or self.privacy in ["public", "unlisted"]: - return True + return # you can see the followers only posts of people you follow if ( self.privacy == "followers" and self.user.followers.filter(id=viewer.id).first() ): - return True + return # you can see dms you are tagged in if hasattr(self, "mention_users"): @@ -75,8 +78,32 @@ class BookWyrmModel(models.Model): self.privacy == "direct" and self.mention_users.filter(id=viewer.id).first() ): - return True - return False + return + raise Http404() + + def raise_not_editable(self, viewer): + """does this user have permission to edit this object? liable to be overwritten + by models that inherit this base model class""" + if not hasattr(self, "user"): + return + + # generally moderators shouldn't be able to edit other people's stuff + if self.user == viewer: + return + + raise PermissionDenied() + + def raise_not_deletable(self, viewer): + """does this user have permission to delete this object? liable to be + overwritten by models that inherit this base model class""" + if not hasattr(self, "user"): + return + + # but generally moderators can delete other people's stuff + if self.user == viewer or viewer.has_perm("moderate_post"): + return + + raise PermissionDenied() @receiver(models.signals.post_save) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 70df3bd4..8ae75baf 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -3,8 +3,8 @@ import re from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.indexes import GinIndex -from django.db import models -from django.db import transaction +from django.db import models, transaction +from django.db.models import Prefetch from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from model_utils import FieldTracker @@ -321,6 +321,27 @@ class Edition(Book): return super().save(*args, **kwargs) + @classmethod + def viewer_aware_objects(cls, viewer): + """annotate a book query with metadata related to the user""" + queryset = cls.objects + if not viewer or not viewer.is_authenticated: + return queryset + + queryset = queryset.prefetch_related( + Prefetch( + "shelfbook_set", + queryset=viewer.shelfbook_set.all(), + to_attr="current_shelves", + ), + Prefetch( + "readthrough_set", + queryset=viewer.readthrough_set.filter(is_active=True).all(), + to_attr="active_readthroughs", + ), + ) + return queryset + def isbn_10_to_13(isbn_10): """convert an isbn 10 into an isbn 13""" diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 49fb5375..022a0d98 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -92,6 +92,12 @@ class ListItem(CollectionItemMixin, BookWyrmModel): notification_type="ADD", ) + def raise_not_deletable(self, viewer): + """the associated user OR the list owner can delete""" + if self.book_list.user == viewer: + return + super().raise_not_deletable(viewer) + class Meta: """A book may only be placed into a list once, and each order in the list may be used only once""" diff --git a/bookwyrm/models/readthrough.py b/bookwyrm/models/readthrough.py index e1090f41..f75918ac 100644 --- a/bookwyrm/models/readthrough.py +++ b/bookwyrm/models/readthrough.py @@ -26,10 +26,14 @@ class ReadThrough(BookWyrmModel): ) start_date = models.DateTimeField(blank=True, null=True) finish_date = models.DateTimeField(blank=True, null=True) + is_active = models.BooleanField(default=True) def save(self, *args, **kwargs): """update user active time""" self.user.update_active_date() + # an active readthrough must have an unset finish date + if self.finish_date: + self.is_active = False super().save(*args, **kwargs) def create_update(self): diff --git a/bookwyrm/models/shelf.py b/bookwyrm/models/shelf.py index 6f134402..c578f082 100644 --- a/bookwyrm/models/shelf.py +++ b/bookwyrm/models/shelf.py @@ -1,5 +1,6 @@ """ puttin' books on shelves """ import re +from django.core.exceptions import PermissionDenied from django.db import models from django.utils import timezone @@ -20,6 +21,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel): name = fields.CharField(max_length=100) identifier = models.CharField(max_length=100) + description = models.TextField(blank=True, null=True, max_length=500) user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="owner" ) @@ -51,12 +53,23 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel): """list of books for this shelf, overrides OrderedCollectionMixin""" return self.books.order_by("shelfbook") + @property + def deletable(self): + """can the shelf be safely deleted?""" + return self.editable and not self.shelfbook_set.exists() + def get_remote_id(self): """shelf identifier instead of id""" base_path = self.user.remote_id identifier = self.identifier or self.get_identifier() return f"{base_path}/books/{identifier}" + def raise_not_deletable(self, viewer): + """don't let anyone delete a default shelf""" + super().raise_not_deletable(viewer) + if not self.deletable: + raise PermissionDenied() + class Meta: """user/shelf unqiueness""" diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 3a0fad5e..b6203678 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -3,6 +3,7 @@ from dataclasses import MISSING import re from django.apps import apps +from django.core.exceptions import PermissionDenied from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.dispatch import receiver @@ -187,6 +188,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): """json serialized activitypub class""" return self.to_activity_dataclass(pure=pure).serialize() + def raise_not_editable(self, viewer): + """certain types of status aren't editable""" + # first, the standard raise + super().raise_not_editable(viewer) + if isinstance(self, (GeneratedNote, ReviewRating)): + raise PermissionDenied() + class GeneratedNote(Status): """these are app-generated messages about user activity""" diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 31c2edf8..895a537a 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -13,7 +13,7 @@ VERSION = "0.0.1" PAGE_LENGTH = env("PAGE_LENGTH", 15) DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") -JS_CACHE = "7f2343cf" +JS_CACHE = "e2bc0653" # email EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") diff --git a/bookwyrm/static/js/status_cache.js b/bookwyrm/static/js/status_cache.js index b3e345b1..2a50bfcb 100644 --- a/bookwyrm/static/js/status_cache.js +++ b/bookwyrm/static/js/status_cache.js @@ -141,8 +141,10 @@ let StatusCache = new class { modal.getElementsByClassName("modal-close")[0].click(); // Update shelve buttons - document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']") - .forEach(button => this.cycleShelveButtons(button, form.reading_status.value)); + if (form.reading_status) { + document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']") + .forEach(button => this.cycleShelveButtons(button, form.reading_status.value)); + } return; } diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index ebfeccf5..5a859e3d 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -236,14 +236,12 @@ {{ form.cover }} - {% if book %}
{{ error | escape }}
{% endfor %} diff --git a/bookwyrm/templates/components/inline_form.html b/bookwyrm/templates/components/inline_form.html index a8924ef2..37f9f556 100644 --- a/bookwyrm/templates/components/inline_form.html +++ b/bookwyrm/templates/components/inline_form.html @@ -1,5 +1,5 @@ {% load i18n %} -{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}
- - {% include 'snippets/goal_form.html' %} -{% trans "No users currently blocked." %}
+{% trans "No users currently blocked." %}
{% else %}{{ form.non_field_errors }}
{% endif %}{{ error | escape }}
- {% endfor %} -{{ error | escape }}
- {% endfor %} -{{ error | escape }}
- {% endfor %} -{{ error | escape }}
- {% endfor %} -{% blocktrans %}Your account will show up in the directory, and may be recommended to other BookWyrm users.{% endblocktrans %}
-{{ error | escape }}
+ {% endfor %} +{{ error | escape }}
+ {% endfor %} +{{ error | escape }}
+ {% endfor %} +{{ error | escape }}
+ {% endfor %} ++ {% blocktrans %}Your account will show up in the directory, and may be recommended to other BookWyrm users.{% endblocktrans %} +
+{% trans "No announcements found." %}
- {% endif %}{% trans "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." %} @@ -55,7 +55,11 @@ {% endfor %} + {% if not domains.exists %} +
{{ error | escape }}
{% endfor %}{{ error | escape }}
{% endfor %}
{% trans "Action" %} | {% if not requests %} -||||
---|---|---|---|---|
{% trans "No requests" %} | ||||
{% trans "No requests" %} | ||||
{% trans "No IP addresses currently blocked" %} |
{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." %}
- {{ site_form.instance_short_description }} -{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." %}
+ {{ site_form.instance_short_description }} +{% trans "(Recommended if registration is open)" %}
-{{ error|escape }}
- {% endfor %} +{% trans "(Recommended if registration is open)" %}
+{{ error|escape }}
+ {% endfor %} +{% trans "This shelf is empty." %}
- {% if shelf.id and shelf.editable %} -{% trans "This shelf is empty." %}
{% endif %}{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}
-{% trans "books" %}
{% trans "books" %}
+{% trans "Goal privacy:" %}
- {% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy %} -- - {% if goal %} - {% trans "Cancel" as button_text %} - {% include 'snippets/toggle/close_button.html' with text=button_text controls_text="show_edit_goal" %} - {% endif %} -
- +{% blocktrans with pages=book.pages %}of {{ pages }} pages{% endblocktrans %}
- {% endif %} -{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}
- - {% include 'snippets/goal_form.html' with goal=goal year=year %} -- {% trans "Back to users" %} -
- -{% endblock %} - -{% block panel %} -{% include 'user_admin/user_info.html' with user=user %} - -{% include 'user_admin/user_moderation_actions.html' with user=user %} - -{% endblock %} - diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index e00a8331..e683f9c2 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -70,6 +70,9 @@ def related_status(notification): @register.simple_tag(takes_context=True) def active_shelf(context, book): """check what shelf a user has a book on, if any""" + if hasattr(book, "current_shelves"): + return book.current_shelves[0] if len(book.current_shelves) else {"book": book} + shelf = ( models.ShelfBook.objects.filter( shelf__user=context["request"].user, @@ -84,8 +87,11 @@ def active_shelf(context, book): @register.simple_tag(takes_context=False) def latest_read_through(book, user): """the most recent read activity""" + if hasattr(book, "active_readthroughs"): + return book.active_readthroughs[0] if len(book.active_readthroughs) else None + return ( - models.ReadThrough.objects.filter(user=user, book=book) + models.ReadThrough.objects.filter(user=user, book=book, is_active=True) .order_by("-start_date") .first() ) diff --git a/bookwyrm/templatetags/utilities.py b/bookwyrm/templatetags/utilities.py index 76b4cd3f..d31f0e4d 100644 --- a/bookwyrm/templatetags/utilities.py +++ b/bookwyrm/templatetags/utilities.py @@ -36,8 +36,10 @@ def get_title(book, too_short=5): @register.simple_tag(takes_context=False) -def comparison_bool(str1, str2): +def comparison_bool(str1, str2, reverse=False): """idk why I need to write a tag for this, it returns a bool""" + if reverse: + return str1 != str2 return str1 == str2 diff --git a/bookwyrm/tests/models/test_base_model.py b/bookwyrm/tests/models/test_base_model.py index 28564740..7c7b48de 100644 --- a/bookwyrm/tests/models/test_base_model.py +++ b/bookwyrm/tests/models/test_base_model.py @@ -1,5 +1,6 @@ """ testing models """ from unittest.mock import patch +from django.http import Http404 from django.test import TestCase from bookwyrm import models @@ -39,14 +40,14 @@ class BaseModel(TestCase): """these should be generated""" self.test_model.id = 1 expected = self.test_model.get_remote_id() - self.assertEqual(expected, "https://%s/bookwyrmtestmodel/1" % DOMAIN) + self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1") def test_remote_id_with_user(self): """format of remote id when there's a user object""" self.test_model.user = self.local_user self.test_model.id = 1 expected = self.test_model.get_remote_id() - self.assertEqual(expected, "https://%s/user/mouse/bookwyrmtestmodel/1" % DOMAIN) + self.assertEqual(expected, f"https://{DOMAIN}/user/mouse/bookwyrmtestmodel/1") def test_set_remote_id(self): """this function sets remote ids after creation""" @@ -55,9 +56,7 @@ class BaseModel(TestCase): instance = models.Work.objects.create(title="work title") instance.remote_id = None base_model.set_remote_id(None, instance, True) - self.assertEqual( - instance.remote_id, "https://%s/book/%d" % (DOMAIN, instance.id) - ) + self.assertEqual(instance.remote_id, f"https://{DOMAIN}/book/{instance.id}") # shouldn't set remote_id if it's not created instance.remote_id = None @@ -70,28 +69,30 @@ class BaseModel(TestCase): obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="public" ) - self.assertTrue(obj.visible_to_user(self.local_user)) + self.assertIsNone(obj.raise_visible_to_user(self.local_user)) obj = models.Shelf.objects.create( name="test", user=self.remote_user, privacy="unlisted" ) - self.assertTrue(obj.visible_to_user(self.local_user)) + self.assertIsNone(obj.raise_visible_to_user(self.local_user)) obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="followers" ) - self.assertFalse(obj.visible_to_user(self.local_user)) + with self.assertRaises(Http404): + obj.raise_visible_to_user(self.local_user) obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="direct" ) - self.assertFalse(obj.visible_to_user(self.local_user)) + with self.assertRaises(Http404): + obj.raise_visible_to_user(self.local_user) obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="direct" ) obj.mention_users.add(self.local_user) - self.assertTrue(obj.visible_to_user(self.local_user)) + self.assertIsNone(obj.raise_visible_to_user(self.local_user)) @patch("bookwyrm.activitystreams.add_status_task.delay") def test_object_visible_to_user_follower(self, _): @@ -100,18 +101,19 @@ class BaseModel(TestCase): obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="followers" ) - self.assertTrue(obj.visible_to_user(self.local_user)) + self.assertIsNone(obj.raise_visible_to_user(self.local_user)) obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="direct" ) - self.assertFalse(obj.visible_to_user(self.local_user)) + with self.assertRaises(Http404): + obj.raise_visible_to_user(self.local_user) obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="direct" ) obj.mention_users.add(self.local_user) - self.assertTrue(obj.visible_to_user(self.local_user)) + self.assertIsNone(obj.raise_visible_to_user(self.local_user)) @patch("bookwyrm.activitystreams.add_status_task.delay") def test_object_visible_to_user_blocked(self, _): @@ -120,9 +122,11 @@ class BaseModel(TestCase): obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="public" ) - self.assertFalse(obj.visible_to_user(self.local_user)) + with self.assertRaises(Http404): + obj.raise_visible_to_user(self.local_user) obj = models.Shelf.objects.create( name="test", user=self.remote_user, privacy="unlisted" ) - self.assertFalse(obj.visible_to_user(self.local_user)) + with self.assertRaises(Http404): + obj.raise_visible_to_user(self.local_user) diff --git a/bookwyrm/tests/views/admin/__init__.py b/bookwyrm/tests/views/admin/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/bookwyrm/tests/views/admin/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/bookwyrm/tests/views/test_announcements.py b/bookwyrm/tests/views/admin/test_announcements.py similarity index 100% rename from bookwyrm/tests/views/test_announcements.py rename to bookwyrm/tests/views/admin/test_announcements.py diff --git a/bookwyrm/tests/views/test_dashboard.py b/bookwyrm/tests/views/admin/test_dashboard.py similarity index 88% rename from bookwyrm/tests/views/test_dashboard.py rename to bookwyrm/tests/views/admin/test_dashboard.py index 6ce30c18..70cc40fe 100644 --- a/bookwyrm/tests/views/test_dashboard.py +++ b/bookwyrm/tests/views/admin/test_dashboard.py @@ -1,5 +1,6 @@ """ test for app action functionality """ from unittest.mock import patch +from tidylib import tidy_document from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -34,5 +35,8 @@ class DashboardViews(TestCase): request.user.is_superuser = True result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_email_blocks.py b/bookwyrm/tests/views/admin/test_email_blocks.py similarity index 86% rename from bookwyrm/tests/views/test_email_blocks.py rename to bookwyrm/tests/views/admin/test_email_blocks.py index be3d9d70..24cdf8a4 100644 --- a/bookwyrm/tests/views/test_email_blocks.py +++ b/bookwyrm/tests/views/admin/test_email_blocks.py @@ -1,5 +1,7 @@ """ test for app action functionality """ from unittest.mock import patch +from tidylib import tidy_document + from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -36,7 +38,10 @@ class EmailBlocklistViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_blocklist_page_post(self): @@ -49,7 +54,10 @@ class EmailBlocklistViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) self.assertTrue( diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/admin/test_federation.py similarity index 90% rename from bookwyrm/tests/views/test_federation.py rename to bookwyrm/tests/views/admin/test_federation.py index ebd311d3..2501a81a 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/admin/test_federation.py @@ -1,6 +1,8 @@ """ test for app action functionality """ import json from unittest.mock import patch +from tidylib import tidy_document + from django.core.files.uploadedfile import SimpleUploadedFile from django.template.response import TemplateResponse from django.test import TestCase @@ -46,10 +48,19 @@ class FederationViews(TestCase): request.user.is_superuser = True result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) - def test_server_page(self): + def test_instance_page(self): """there are so many views, this just makes sure it LOADS""" server = models.FederatedServer.objects.create(server_name="hi.there.com") view = views.FederatedServer.as_view() @@ -59,7 +70,10 @@ class FederationViews(TestCase): result = view(request, server.id) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_server_page_block(self): @@ -148,7 +162,10 @@ class FederationViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_add_view_post_create(self): @@ -169,6 +186,7 @@ class FederationViews(TestCase): self.assertEqual(server.application_type, "coolsoft") self.assertEqual(server.status, "blocked") + # pylint: disable=consider-using-with def test_import_blocklist(self): """load a json file with a list of servers to block""" server = models.FederatedServer.objects.create(server_name="hi.there.com") @@ -180,7 +198,7 @@ class FederationViews(TestCase): {"instance": "hi.there.com", "url": "https://explanation.url"}, # existing {"a": "b"}, # invalid ] - json.dump(data, open("file.json", "w")) + json.dump(data, open("file.json", "w")) # pylint: disable=unspecified-encoding view = views.ImportServerBlocklist.as_view() request = self.factory.post( diff --git a/bookwyrm/tests/views/admin/test_ip_blocklist.py b/bookwyrm/tests/views/admin/test_ip_blocklist.py new file mode 100644 index 00000000..fb249b76 --- /dev/null +++ b/bookwyrm/tests/views/admin/test_ip_blocklist.py @@ -0,0 +1,44 @@ +""" test for app action functionality """ +from unittest.mock import patch +from tidylib import tidy_document +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models, views + + +class IPBlocklistViews(TestCase): + """every response to a get request, html or json""" + + def setUp(self): + """we need basic test data and mocks""" + self.factory = RequestFactory() + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ): + 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_blocklist_page_get(self): + """there are so many views, this just makes sure it LOADS""" + view = views.IPBlocklist.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + + result = view(request) + + self.assertIsInstance(result, TemplateResponse) + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) + self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/admin/test_reports.py similarity index 87% rename from bookwyrm/tests/views/test_reports.py rename to bookwyrm/tests/views/admin/test_reports.py index 9fbeae04..456dff55 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/admin/test_reports.py @@ -1,6 +1,8 @@ """ test for app action functionality """ import json from unittest.mock import patch +from tidylib import tidy_document + from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -42,7 +44,16 @@ class ReportViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_reports_page_with_data(self): @@ -55,7 +66,16 @@ class ReportViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_report_page(self): @@ -69,7 +89,10 @@ class ReportViews(TestCase): result = view(request, report.id) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_report_comment(self): diff --git a/bookwyrm/tests/views/test_user_admin.py b/bookwyrm/tests/views/admin/test_user_admin.py similarity index 82% rename from bookwyrm/tests/views/test_user_admin.py rename to bookwyrm/tests/views/admin/test_user_admin.py index 3917a6fd..3336cf24 100644 --- a/bookwyrm/tests/views/test_user_admin.py +++ b/bookwyrm/tests/views/admin/test_user_admin.py @@ -1,5 +1,7 @@ """ test for app action functionality """ from unittest.mock import patch +from tidylib import tidy_document + from django.contrib.auth.models import Group from django.template.response import TemplateResponse from django.test import TestCase @@ -34,7 +36,10 @@ class UserAdminViews(TestCase): request.user.is_superuser = True result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_user_admin_page(self): @@ -47,7 +52,10 @@ class UserAdminViews(TestCase): result = view(request, self.local_user.id) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @@ -69,7 +77,10 @@ class UserAdminViews(TestCase): result = view(request, self.local_user.id) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) self.assertEqual( list(self.local_user.groups.values_list("name", flat=True)), ["editor"] diff --git a/bookwyrm/tests/views/inbox/test_inbox.py b/bookwyrm/tests/views/inbox/test_inbox.py index a84458ab..47e6a86e 100644 --- a/bookwyrm/tests/views/inbox/test_inbox.py +++ b/bookwyrm/tests/views/inbox/test_inbox.py @@ -3,6 +3,7 @@ import json import pathlib from unittest.mock import patch +from django.core.exceptions import PermissionDenied from django.http import HttpResponseNotAllowed, HttpResponseNotFound from django.test import TestCase, Client from django.test.client import RequestFactory @@ -130,22 +131,24 @@ class Inbox(TestCase): "", HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", ) - self.assertFalse(views.inbox.is_blocked_user_agent(request)) + self.assertIsNone(views.inbox.raise_is_blocked_user_agent(request)) models.FederatedServer.objects.create( server_name="mastodon.social", status="blocked" ) - self.assertTrue(views.inbox.is_blocked_user_agent(request)) + with self.assertRaises(PermissionDenied): + views.inbox.raise_is_blocked_user_agent(request) def test_is_blocked_activity(self): """check for blocked servers""" activity = {"actor": "https://mastodon.social/user/whaatever/else"} - self.assertFalse(views.inbox.is_blocked_activity(activity)) + self.assertIsNone(views.inbox.raise_is_blocked_activity(activity)) models.FederatedServer.objects.create( server_name="mastodon.social", status="blocked" ) - self.assertTrue(views.inbox.is_blocked_activity(activity)) + with self.assertRaises(PermissionDenied): + views.inbox.raise_is_blocked_activity(activity) @patch("bookwyrm.suggested_users.remove_user_task.delay") def test_create_by_deactivated_user(self, _): @@ -157,11 +160,11 @@ class Inbox(TestCase): activity = self.create_json activity["actor"] = self.remote_user.remote_id activity["object"] = status_data + activity["type"] = "Create" - with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: - mock_valid.return_value = True - - result = self.client.post( - "/inbox", json.dumps(activity), content_type="application/json" - ) - self.assertEqual(result.status_code, 403) + response = self.client.post( + "/inbox", + json.dumps(activity), + content_type="application/json", + ) + self.assertEqual(response.status_code, 403) diff --git a/bookwyrm/tests/views/preferences/__init__.py b/bookwyrm/tests/views/preferences/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/bookwyrm/tests/views/preferences/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/bookwyrm/tests/views/test_block.py b/bookwyrm/tests/views/preferences/test_block.py similarity index 93% rename from bookwyrm/tests/views/test_block.py rename to bookwyrm/tests/views/preferences/test_block.py index f1ec42f0..6663aa63 100644 --- a/bookwyrm/tests/views/test_block.py +++ b/bookwyrm/tests/views/preferences/test_block.py @@ -1,5 +1,7 @@ """ test for app action functionality """ from unittest.mock import patch +from tidylib import tidy_document + from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -44,7 +46,10 @@ class BlockViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_block_post(self, _): @@ -75,6 +80,6 @@ class BlockViews(TestCase): request.user = self.local_user with patch("bookwyrm.activitystreams.add_user_statuses_task.delay"): - views.block.unblock(request, self.remote_user.id) + views.unblock(request, self.remote_user.id) self.assertFalse(models.UserBlocks.objects.exists()) diff --git a/bookwyrm/tests/views/preferences/test_change_password.py b/bookwyrm/tests/views/preferences/test_change_password.py new file mode 100644 index 00000000..17afb8f7 --- /dev/null +++ b/bookwyrm/tests/views/preferences/test_change_password.py @@ -0,0 +1,61 @@ +""" test for app action functionality """ +from unittest.mock import patch +from tidylib import tidy_document + +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models, views + + +class ChangePasswordViews(TestCase): + """view user and edit profile""" + + def setUp(self): + """we need basic test data and mocks""" + self.factory = RequestFactory() + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ): + self.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.com", + "password", + local=True, + localname="mouse", + ) + models.SiteSettings.objects.create(id=1) + + def test_password_change_get(self): + """there are so many views, this just makes sure it LOADS""" + view = views.ChangePassword.as_view() + request = self.factory.get("") + request.user = self.local_user + + result = view(request) + self.assertIsInstance(result, TemplateResponse) + html = result.render() + _, errors = tidy_document(html.content) + if errors: + raise Exception(errors) + self.assertEqual(result.status_code, 200) + + def test_password_change(self): + """change password""" + view = views.ChangePassword.as_view() + password_hash = self.local_user.password + request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) + request.user = self.local_user + with patch("bookwyrm.views.preferences.change_password.login"): + view(request) + self.assertNotEqual(self.local_user.password, password_hash) + + def test_password_change_mismatch(self): + """change password""" + view = views.ChangePassword.as_view() + password_hash = self.local_user.password + request = self.factory.post("", {"password": "hi", "confirm-password": "hihi"}) + request.user = self.local_user + view(request) + self.assertEqual(self.local_user.password, password_hash) diff --git a/bookwyrm/tests/views/preferences/test_delete_user.py b/bookwyrm/tests/views/preferences/test_delete_user.py new file mode 100644 index 00000000..40a28d44 --- /dev/null +++ b/bookwyrm/tests/views/preferences/test_delete_user.py @@ -0,0 +1,89 @@ +""" test for app action functionality """ +import json +from unittest.mock import patch +from tidylib import tidy_document + +from django.contrib.sessions.middleware import SessionMiddleware +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import forms, models, views + + +@patch("bookwyrm.suggested_users.remove_user_task.delay") +class DeleteUserViews(TestCase): + """view user and edit profile""" + + def setUp(self): + """we need basic test data and mocks""" + self.factory = RequestFactory() + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ): + self.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) + self.rat = models.User.objects.create_user( + "rat@local.com", "rat@rat.rat", "password", local=True, localname="rat" + ) + + self.book = models.Edition.objects.create( + title="test", parent_work=models.Work.objects.create(title="test work") + ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"), patch( + "bookwyrm.activitystreams.add_book_statuses_task.delay" + ): + models.ShelfBook.objects.create( + book=self.book, + user=self.local_user, + shelf=self.local_user.shelf_set.first(), + ) + + models.SiteSettings.objects.create() + + def test_delete_user_page(self, _): + """there are so many views, this just makes sure it LOADS""" + view = views.DeleteUser.as_view() + request = self.factory.get("") + request.user = self.local_user + result = view(request) + self.assertIsInstance(result, TemplateResponse) + html = result.render() + _, errors = tidy_document(html.content) + if errors: + raise Exception(errors) + self.assertEqual(result.status_code, 200) + + @patch("bookwyrm.suggested_users.rerank_suggestions_task") + def test_delete_user(self, *_): + """use a form to update a user""" + view = views.DeleteUser.as_view() + form = forms.DeleteUserForm() + form.data["password"] = "password" + request = self.factory.post("", form.data) + request.user = self.local_user + middleware = SessionMiddleware() + middleware.process_request(request) + request.session.save() + + self.assertIsNone(self.local_user.name) + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as delay_mock: + view(request) + self.assertEqual(delay_mock.call_count, 1) + activity = json.loads(delay_mock.call_args[0][1]) + self.assertEqual(activity["type"], "Delete") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual( + activity["cc"][0], "https://www.w3.org/ns/activitystreams#Public" + ) + + self.local_user.refresh_from_db() + self.assertFalse(self.local_user.is_active) + self.assertEqual(self.local_user.deactivation_reason, "self_deletion") diff --git a/bookwyrm/tests/views/test_edit_user.py b/bookwyrm/tests/views/preferences/test_edit_user.py similarity index 71% rename from bookwyrm/tests/views/test_edit_user.py rename to bookwyrm/tests/views/preferences/test_edit_user.py index 07e54e4e..3dccf518 100644 --- a/bookwyrm/tests/views/test_edit_user.py +++ b/bookwyrm/tests/views/preferences/test_edit_user.py @@ -1,11 +1,10 @@ """ test for app action functionality """ -import json import pathlib from unittest.mock import patch from PIL import Image +from tidylib import tidy_document from django.contrib.auth.models import AnonymousUser -from django.contrib.sessions.middleware import SessionMiddleware from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.template.response import TemplateResponse @@ -59,7 +58,10 @@ class EditUserViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_edit_user(self, _): @@ -91,8 +93,9 @@ class EditUserViews(TestCase): form.data["default_post_privacy"] = "public" form.data["preferred_timezone"] = "UTC" image_file = pathlib.Path(__file__).parent.joinpath( - "../../static/images/no_cover.jpg" + "../../../static/images/no_cover.jpg" ) + # pylint: disable=consider-using-with form.data["avatar"] = SimpleUploadedFile( image_file, open(image_file, "rb").read(), content_type="image/jpeg" ) @@ -113,50 +116,11 @@ class EditUserViews(TestCase): def test_crop_avatar(self, _): """reduce that image size""" image_file = pathlib.Path(__file__).parent.joinpath( - "../../static/images/no_cover.jpg" + "../../../static/images/no_cover.jpg" ) image = Image.open(image_file) - result = views.edit_user.crop_avatar(image) + result = views.preferences.edit_user.crop_avatar(image) self.assertIsInstance(result, ContentFile) image_result = Image.open(result) self.assertEqual(image_result.size, (120, 120)) - - def test_delete_user_page(self, _): - """there are so many views, this just makes sure it LOADS""" - view = views.DeleteUser.as_view() - request = self.factory.get("") - request.user = self.local_user - result = view(request) - self.assertIsInstance(result, TemplateResponse) - result.render() - self.assertEqual(result.status_code, 200) - - @patch("bookwyrm.suggested_users.rerank_suggestions_task") - def test_delete_user(self, *_): - """use a form to update a user""" - view = views.DeleteUser.as_view() - form = forms.DeleteUserForm() - form.data["password"] = "password" - request = self.factory.post("", form.data) - request.user = self.local_user - middleware = SessionMiddleware() - middleware.process_request(request) - request.session.save() - - self.assertIsNone(self.local_user.name) - with patch( - "bookwyrm.models.activitypub_mixin.broadcast_task.delay" - ) as delay_mock: - view(request) - self.assertEqual(delay_mock.call_count, 1) - activity = json.loads(delay_mock.call_args[0][1]) - self.assertEqual(activity["type"], "Delete") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual( - activity["cc"][0], "https://www.w3.org/ns/activitystreams#Public" - ) - - self.local_user.refresh_from_db() - self.assertFalse(self.local_user.is_active) - self.assertEqual(self.local_user.deactivation_reason, "self_deletion") diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index 99022ec5..cf86a596 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -9,6 +9,7 @@ import responses from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from django.core.files.uploadedfile import SimpleUploadedFile +from django.http import Http404 from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -133,8 +134,8 @@ class BookViews(TestCase): request.user = self.local_user with patch("bookwyrm.views.books.is_api_request") as is_api: is_api.return_value = False - result = view(request, 0) - self.assertEqual(result.status_code, 404) + with self.assertRaises(Http404): + view(request, 0) def test_book_page_work_id(self): """there are so many views, this just makes sure it LOADS""" @@ -282,6 +283,46 @@ class BookViews(TestCase): self.assertEqual(book.authors.first().name, "Sappho") self.assertEqual(book.authors.first(), book.parent_work.authors.first()) + def _setup_cover_url(self): + cover_url = "http://example.com" + image_file = pathlib.Path(__file__).parent.joinpath( + "../../static/images/default_avi.jpg" + ) + image = Image.open(image_file) + output = BytesIO() + image.save(output, format=image.format) + responses.add( + responses.GET, + cover_url, + body=output.getvalue(), + status=200, + ) + return cover_url + + @responses.activate + def test_create_book_upload_cover_url(self): + """create an entirely new book and work with cover url""" + self.assertFalse(self.book.cover) + view = views.ConfirmEditBook.as_view() + self.local_user.groups.add(self.group) + cover_url = self._setup_cover_url() + + form = forms.EditionForm() + form.data["title"] = "New Title" + form.data["last_edited_by"] = self.local_user.id + form.data["cover-url"] = cover_url + request = self.factory.post("", form.data) + request.user = self.local_user + + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as delay_mock: + views.upload_cover(request, self.book.id) + self.assertEqual(delay_mock.call_count, 1) + + self.book.refresh_from_db() + self.assertTrue(self.book.cover) + def test_upload_cover_file(self): """add a cover via file upload""" self.assertFalse(self.book.cover) @@ -310,21 +351,8 @@ class BookViews(TestCase): def test_upload_cover_url(self): """add a cover via url""" self.assertFalse(self.book.cover) - image_file = pathlib.Path(__file__).parent.joinpath( - "../../static/images/default_avi.jpg" - ) - image = Image.open(image_file) - output = BytesIO() - image.save(output, format=image.format) - responses.add( - responses.GET, - "http://example.com", - body=output.getvalue(), - status=200, - ) - form = forms.CoverForm(instance=self.book) - form.data["cover-url"] = "http://example.com" + form.data["cover-url"] = self._setup_cover_url() request = self.factory.post("", form.data) request.user = self.local_user diff --git a/bookwyrm/tests/views/test_directory.py b/bookwyrm/tests/views/test_directory.py index 90638b0a..0193d19d 100644 --- a/bookwyrm/tests/views/test_directory.py +++ b/bookwyrm/tests/views/test_directory.py @@ -1,5 +1,6 @@ """ test for app action functionality """ from unittest.mock import patch +from tidylib import tidy_document from django.contrib.auth.models import AnonymousUser from django.template.response import TemplateResponse @@ -51,7 +52,16 @@ class DirectoryViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_directory_page_empty(self): @@ -62,7 +72,10 @@ class DirectoryViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document(html.content, options={"drop-empty-elements": False}) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) def test_directory_page_logged_out(self): diff --git a/bookwyrm/tests/views/test_feed.py b/bookwyrm/tests/views/test_feed.py index 526cb0f9..a6f220b5 100644 --- a/bookwyrm/tests/views/test_feed.py +++ b/bookwyrm/tests/views/test_feed.py @@ -5,6 +5,7 @@ import pathlib from PIL import Image from django.core.files.base import ContentFile +from django.http import Http404 from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -81,9 +82,8 @@ class FeedViews(TestCase): request.user = self.local_user with patch("bookwyrm.views.feed.is_api_request") as is_api: is_api.return_value = False - result = view(request, "mouse", 12345) - - self.assertEqual(result.status_code, 404) + with self.assertRaises(Http404): + view(request, "mouse", 12345) def test_status_page_not_found_wrong_user(self, *_): """there are so many views, this just makes sure it LOADS""" @@ -102,9 +102,8 @@ class FeedViews(TestCase): request.user = self.local_user with patch("bookwyrm.views.feed.is_api_request") as is_api: is_api.return_value = False - result = view(request, "mouse", status.id) - - self.assertEqual(result.status_code, 404) + with self.assertRaises(Http404): + view(request, "mouse", status.id) def test_status_page_with_image(self, *_): """there are so many views, this just makes sure it LOADS""" diff --git a/bookwyrm/tests/views/test_goal.py b/bookwyrm/tests/views/test_goal.py index 2ecce0ac..741fca9c 100644 --- a/bookwyrm/tests/views/test_goal.py +++ b/bookwyrm/tests/views/test_goal.py @@ -1,11 +1,13 @@ """ test for app action functionality """ from unittest.mock import patch -from django.utils import timezone +from tidylib import tidy_document from django.contrib.auth.models import AnonymousUser +from django.http import Http404 from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory +from django.utils import timezone from bookwyrm import models, views @@ -60,7 +62,16 @@ class GoalViews(TestCase): request.user = self.local_user result = view(request, self.local_user.localname, self.year) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertIsInstance(result, TemplateResponse) def test_goal_page_anonymous(self): @@ -91,7 +102,16 @@ class GoalViews(TestCase): request.user = self.rat result = view(request, self.local_user.localname, timezone.now().year) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertIsInstance(result, TemplateResponse) def test_goal_page_private(self): @@ -103,8 +123,8 @@ class GoalViews(TestCase): request = self.factory.get("") request.user = self.rat - result = view(request, self.local_user.localname, self.year) - self.assertEqual(result.status_code, 404) + with self.assertRaises(Http404): + view(request, self.local_user.localname, self.year) @patch("bookwyrm.activitystreams.add_status_task.delay") def test_create_goal(self, _): diff --git a/bookwyrm/tests/views/test_list_actions.py b/bookwyrm/tests/views/test_list_actions.py index 58515681..f7775d19 100644 --- a/bookwyrm/tests/views/test_list_actions.py +++ b/bookwyrm/tests/views/test_list_actions.py @@ -568,5 +568,6 @@ class ListActionViews(TestCase): ) request.user = self.rat - views.list.remove_book(request, self.list.id) + with self.assertRaises(PermissionDenied): + views.list.remove_book(request, self.list.id) self.assertTrue(self.list.listitem_set.exists()) diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py index b07d98a7..47d8bb27 100644 --- a/bookwyrm/tests/views/test_password.py +++ b/bookwyrm/tests/views/test_password.py @@ -43,12 +43,14 @@ class PasswordViews(TestCase): def test_password_reset_request_post(self): """send 'em an email""" request = self.factory.post("", {"email": "aa@bb.ccc"}) + request.user = self.anonymous_user view = views.PasswordResetRequest.as_view() resp = view(request) self.assertEqual(resp.status_code, 200) resp.render() request = self.factory.post("", {"email": "mouse@mouse.com"}) + request.user = self.anonymous_user with patch("bookwyrm.emailing.send_email.delay"): resp = view(request) resp.render() @@ -93,33 +95,3 @@ class PasswordViews(TestCase): resp = view(request, code.code) resp.render() self.assertTrue(models.PasswordReset.objects.exists()) - - def test_password_change_get(self): - """there are so many views, this just makes sure it LOADS""" - view = views.ChangePassword.as_view() - request = self.factory.get("") - request.user = self.local_user - - result = view(request) - self.assertIsInstance(result, TemplateResponse) - result.render() - self.assertEqual(result.status_code, 200) - - def test_password_change(self): - """change password""" - view = views.ChangePassword.as_view() - password_hash = self.local_user.password - request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) - request.user = self.local_user - with patch("bookwyrm.views.password.login"): - view(request) - self.assertNotEqual(self.local_user.password, password_hash) - - def test_password_change_mismatch(self): - """change password""" - view = views.ChangePassword.as_view() - password_hash = self.local_user.password - request = self.factory.post("", {"password": "hi", "confirm-password": "hihi"}) - request.user = self.local_user - view(request) - self.assertEqual(self.local_user.password, password_hash) diff --git a/bookwyrm/tests/views/test_reading.py b/bookwyrm/tests/views/test_reading.py index 3f5846d0..52d10aa6 100644 --- a/bookwyrm/tests/views/test_reading.py +++ b/bookwyrm/tests/views/test_reading.py @@ -113,6 +113,7 @@ class ReadingViews(TestCase): { "post-status": True, "privacy": "followers", + "start_date": readthrough.start_date, "finish_date": timezone.now().isoformat(), "id": readthrough.id, }, diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index d873edc6..d769d93c 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -1,11 +1,14 @@ """ test for app action functionality """ import json from unittest.mock import patch +from tidylib import tidy_document + +from django.core.exceptions import PermissionDenied from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import models, views +from bookwyrm import forms, models, views from bookwyrm.activitypub import ActivitypubResponse @@ -53,7 +56,16 @@ class ShelfViews(TestCase): is_api.return_value = False result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) with patch("bookwyrm.views.shelf.is_api_request") as is_api: @@ -105,7 +117,7 @@ class ShelfViews(TestCase): shelf.refresh_from_db() self.assertEqual(shelf.name, "cool name") - self.assertEqual(shelf.identifier, "testshelf-%d" % shelf.id) + self.assertEqual(shelf.identifier, f"testshelf-{shelf.id}") def test_edit_shelf_name_not_editable(self, *_): """can't change the name of an non-editable shelf""" @@ -122,7 +134,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.name, "To Read") - def test_handle_shelve(self, *_): + def test_shelve(self, *_): """shelve a book""" request = self.factory.post( "", {"book": self.book.id, "shelf": self.shelf.identifier} @@ -140,7 +152,7 @@ class ShelfViews(TestCase): # make sure the book is on the shelf self.assertEqual(self.shelf.books.get(), self.book) - def test_handle_shelve_to_read(self, *_): + def test_shelve_to_read(self, *_): """special behavior for the to-read shelf""" shelf = models.Shelf.objects.get(identifier="to-read") request = self.factory.post( @@ -153,7 +165,7 @@ class ShelfViews(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - def test_handle_shelve_reading(self, *_): + def test_shelve_reading(self, *_): """special behavior for the reading shelf""" shelf = models.Shelf.objects.get(identifier="reading") request = self.factory.post( @@ -166,7 +178,7 @@ class ShelfViews(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - def test_handle_shelve_read(self, *_): + def test_shelve_read(self, *_): """special behavior for the read shelf""" shelf = models.Shelf.objects.get(identifier="read") request = self.factory.post( @@ -179,7 +191,7 @@ class ShelfViews(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - def test_handle_unshelve(self, *_): + def test_unshelve(self, *_): """remove a book from a shelf""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( @@ -197,3 +209,76 @@ class ShelfViews(TestCase): self.assertEqual(activity["type"], "Remove") self.assertEqual(activity["object"]["id"], item.remote_id) self.assertEqual(self.shelf.books.count(), 0) + + def test_create_shelf(self, *_): + """a brand new custom shelf""" + form = forms.ShelfForm() + form.data["user"] = self.local_user.id + form.data["name"] = "new shelf name" + form.data["description"] = "desc" + form.data["privacy"] = "unlisted" + request = self.factory.post("", form.data) + request.user = self.local_user + + views.create_shelf(request) + + shelf = models.Shelf.objects.get(name="new shelf name") + self.assertEqual(shelf.privacy, "unlisted") + self.assertEqual(shelf.description, "desc") + self.assertEqual(shelf.user, self.local_user) + + def test_delete_shelf(self, *_): + """delete a brand new custom shelf""" + request = self.factory.post("") + request.user = self.local_user + shelf_id = self.shelf.id + + views.delete_shelf(request, shelf_id) + + self.assertFalse(models.Shelf.objects.filter(id=shelf_id).exists()) + + def test_delete_shelf_unauthorized(self, *_): + """delete a brand new custom shelf""" + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ): + rat = models.User.objects.create_user( + "rat@local.com", + "rat@mouse.mouse", + "password", + local=True, + localname="rat", + ) + request = self.factory.post("") + request.user = rat + + with self.assertRaises(PermissionDenied): + views.delete_shelf(request, self.shelf.id) + + self.assertTrue(models.Shelf.objects.filter(id=self.shelf.id).exists()) + + def test_delete_shelf_has_book(self, *_): + """delete a brand new custom shelf""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ShelfBook.objects.create( + book=self.book, user=self.local_user, shelf=self.shelf + ) + request = self.factory.post("") + request.user = self.local_user + + with self.assertRaises(PermissionDenied): + views.delete_shelf(request, self.shelf.id) + + self.assertTrue(models.Shelf.objects.filter(id=self.shelf.id).exists()) + + def test_delete_shelf_not_editable(self, *_): + """delete a brand new custom shelf""" + shelf = self.local_user.shelf_set.first() + self.assertFalse(shelf.editable) + request = self.factory.post("") + request.user = self.local_user + + with self.assertRaises(PermissionDenied): + views.delete_shelf(request, shelf.id) + + self.assertTrue(models.Shelf.objects.filter(id=shelf.id).exists()) diff --git a/bookwyrm/tests/views/test_status.py b/bookwyrm/tests/views/test_status.py index 592d1f5c..6a09807c 100644 --- a/bookwyrm/tests/views/test_status.py +++ b/bookwyrm/tests/views/test_status.py @@ -1,6 +1,7 @@ """ test for app action functionality """ import json from unittest.mock import patch +from django.core.exceptions import PermissionDenied from django.test import TestCase from django.test.client import RequestFactory @@ -196,9 +197,9 @@ class StatusViews(TestCase): ) with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock: - result = view(request, status.id) + with self.assertRaises(PermissionDenied): + view(request, status.id) self.assertFalse(mock.called) - self.assertEqual(result.status_code, 400) status.refresh_from_db() self.assertFalse(status.deleted) @@ -214,9 +215,9 @@ class StatusViews(TestCase): ) with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock: - result = view(request, status.id) + with self.assertRaises(PermissionDenied): + view(request, status.id) self.assertFalse(mock.called) - self.assertEqual(result.status_code, 400) status.refresh_from_db() self.assertFalse(status.deleted) @@ -375,7 +376,8 @@ http://www.fish.com/""" request = self.factory.post("") request.user = self.remote_user - view(request, status.id) + with self.assertRaises(PermissionDenied): + view(request, status.id) status.refresh_from_db() self.assertFalse(status.deleted) diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index 614f772d..f2d9b861 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -1,5 +1,6 @@ """ test for app action functionality """ from unittest.mock import patch +from tidylib import tidy_document from django.contrib.auth.models import AnonymousUser from django.http.response import Http404 @@ -55,7 +56,16 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, "mouse") self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) request.user = self.anonymous_user @@ -63,7 +73,16 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, "mouse") self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) with patch("bookwyrm.views.user.is_api_request") as is_api: @@ -92,7 +111,16 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, "mouse") self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) with patch("bookwyrm.views.user.is_api_request") as is_api: @@ -123,7 +151,16 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, "mouse") self.assertIsInstance(result, TemplateResponse) - result.render() + html = result.render() + _, errors = tidy_document( + html.content, + options={ + "drop-empty-elements": False, + "warn-proprietary-attributes": False, + }, + ) + if errors: + raise Exception(errors) self.assertEqual(result.status_code, 200) with patch("bookwyrm.views.user.is_api_request") as is_api: diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index d54347f0..360f147f 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -43,6 +43,7 @@ urlpatterns = [ re_path(r"^nodeinfo/2\.0/?$", views.nodeinfo), re_path(r"^api/v1/instance/?$", views.instance_info), re_path(r"^api/v1/instance/peers/?$", views.peers), + re_path(r"^opensearch.xml$", views.opensearch, name="opensearch"), # polling updates re_path("^api/updates/notifications/?$", views.get_notification_count), re_path("^api/updates/stream/(?P