From 385ec4d70a4f42eaeb1c9e906ec95e4be5eaf024 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 1 Jan 2021 11:05:49 -0800 Subject: [PATCH 001/145] Adds ReviewRating model I can't just calling Rating because that would clash with the rating field --- bookwyrm/activitypub/__init__.py | 3 +- bookwyrm/activitypub/note.py | 16 ++++++-- bookwyrm/forms.py | 4 +- bookwyrm/migrations/0030_reviewrating.py | 52 ++++++++++++++++++++++++ bookwyrm/models/__init__.py | 3 +- bookwyrm/models/status.py | 16 ++++++++ 6 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 bookwyrm/migrations/0030_reviewrating.py diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index a4fef41e5..49e278bf7 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -6,7 +6,8 @@ from .base_activity import ActivityEncoder, Signature from .base_activity import Link, Mention from .base_activity import ActivitySerializerError, resolve_remote_id from .image import Image -from .note import Note, GeneratedNote, Article, Comment, Review, Quotation +from .note import Note, GeneratedNote, Article, Comment, Quotation +from .note import Review, Rating from .note import Tombstone from .interaction import Boost, Like from .ordered_collection import OrderedCollection, OrderedCollectionPage diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 72fbe5fc2..cbbeb779f 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -50,6 +50,13 @@ class Comment(Note): type: str = 'Comment' +@dataclass(init=False) +class Quotation(Comment): + ''' a quote and commentary on a book ''' + quote: str + type: str = 'Quotation' + + @dataclass(init=False) class Review(Comment): ''' a full book review ''' @@ -59,7 +66,8 @@ class Review(Comment): @dataclass(init=False) -class Quotation(Comment): - ''' a quote and commentary on a book ''' - quote: str - type: str = 'Quotation' +class Rating(Comment): + ''' a full book review ''' + rating: int = None + content: str = None + type: str = 'Rating' diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 686ac8b1d..133f75430 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -54,8 +54,8 @@ class RegisterForm(CustomForm): class RatingForm(CustomForm): class Meta: - model = models.Review - fields = ['user', 'book', 'content', 'rating', 'privacy'] + model = models.ReviewRating + fields = ['user', 'book', 'rating', 'privacy'] class ReviewForm(CustomForm): diff --git a/bookwyrm/migrations/0030_reviewrating.py b/bookwyrm/migrations/0030_reviewrating.py new file mode 100644 index 000000000..3658a666e --- /dev/null +++ b/bookwyrm/migrations/0030_reviewrating.py @@ -0,0 +1,52 @@ +# Generated by Django 3.0.7 on 2021-01-01 19:05 + +from django.db.models.fields.reverse_related import ManyToOneRel +from django.db.models import Q +from django.db import migrations, models +import django.db.models.deletion +from bookwyrm.management.commands.deduplicate_book_data import update_related + + +def convert_review_rating(app_registry, schema_editor): + ''' take reviews with no content and turn them into ratings ''' + db_alias = schema_editor.connection.alias + reviews = app_registry.get_model('bookwyrm', 'Review') + review_ratings = app_registry.get_model('bookwyrm', 'ReviewRating') + ratings = reviews.objects.using(db_alias).filter( + Q(content__isnull=True) | Q(content='')) + # replace the old review with the rating + for review in ratings: + rating = review_ratings.objects.create( + user=review.user, rating=review.rating, book=review.book) + for field in review._meta.get_fields(): + if isinstance(field, ManyToOneRel) or field.name == 'status_ptr': + continue + value = getattr(review, field.name) + print(review, rating, field.name, value) + try: + setattr(rating, field.name, value) + except TypeError: + getattr(rating, field.name).set(value.all()) + rating.save() + update_related(rating, review) + review.delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0029_auto_20201221_2014'), + ] + + operations = [ + migrations.CreateModel( + name='ReviewRating', + fields=[ + ('review_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Review')), + ], + options={ + 'abstract': False, + }, + bases=('bookwyrm.review',), + ), + migrations.RunPython(convert_review_rating), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 48852cfe4..4a0db4f55 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -8,7 +8,8 @@ from .connector import Connector from .shelf import Shelf, ShelfBook -from .status import Status, GeneratedNote, Review, Comment, Quotation +from .status import Status, GeneratedNote, Comment, Quotation +from .status import Review, ReviewRating from .status import Boost from .attachment import Image from .favorite import Favorite diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 10cee7525..ee8faf005 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -222,6 +222,22 @@ class Review(Status): pure_type = 'Article' +class ReviewRating(Review): + ''' a subtype of review that only contains a rating ''' + def save(self, *args, **kwargs): + if not self.rating: + raise ValueError('Rating object must include a numerical rating') + return super().save(*args, **kwargs) + + @property + def pure_content(self): + #pylint: disable=bad-string-format-type + return 'Rated "%s": %d' % (self.book.title, self.rating) + + activity_serializer = activitypub.Rating + pure_type = 'Note' + + class Boost(Status): ''' boost'ing a post ''' boosted_status = fields.ForeignKey( From dad202823aa73bbfeea9744a518832ff4a7ee6cc Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 1 Jan 2021 15:36:43 -0800 Subject: [PATCH 002/145] Moves review re-structing into separate migration --- bookwyrm/migrations/0030_reviewrating.py | 29 --------- .../migrations/0031_auto_20210101_2109.py | 61 +++++++++++++++++++ 2 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 bookwyrm/migrations/0031_auto_20210101_2109.py diff --git a/bookwyrm/migrations/0030_reviewrating.py b/bookwyrm/migrations/0030_reviewrating.py index 3658a666e..327c6c6a8 100644 --- a/bookwyrm/migrations/0030_reviewrating.py +++ b/bookwyrm/migrations/0030_reviewrating.py @@ -1,35 +1,7 @@ # Generated by Django 3.0.7 on 2021-01-01 19:05 -from django.db.models.fields.reverse_related import ManyToOneRel -from django.db.models import Q from django.db import migrations, models import django.db.models.deletion -from bookwyrm.management.commands.deduplicate_book_data import update_related - - -def convert_review_rating(app_registry, schema_editor): - ''' take reviews with no content and turn them into ratings ''' - db_alias = schema_editor.connection.alias - reviews = app_registry.get_model('bookwyrm', 'Review') - review_ratings = app_registry.get_model('bookwyrm', 'ReviewRating') - ratings = reviews.objects.using(db_alias).filter( - Q(content__isnull=True) | Q(content='')) - # replace the old review with the rating - for review in ratings: - rating = review_ratings.objects.create( - user=review.user, rating=review.rating, book=review.book) - for field in review._meta.get_fields(): - if isinstance(field, ManyToOneRel) or field.name == 'status_ptr': - continue - value = getattr(review, field.name) - print(review, rating, field.name, value) - try: - setattr(rating, field.name, value) - except TypeError: - getattr(rating, field.name).set(value.all()) - rating.save() - update_related(rating, review) - review.delete() class Migration(migrations.Migration): @@ -48,5 +20,4 @@ class Migration(migrations.Migration): }, bases=('bookwyrm.review',), ), - migrations.RunPython(convert_review_rating), ] diff --git a/bookwyrm/migrations/0031_auto_20210101_2109.py b/bookwyrm/migrations/0031_auto_20210101_2109.py new file mode 100644 index 000000000..8343719f7 --- /dev/null +++ b/bookwyrm/migrations/0031_auto_20210101_2109.py @@ -0,0 +1,61 @@ +# Generated by Django 3.0.7 on 2021-01-01 21:09 + +from django.db.models.fields.reverse_related import ManyToOneRel +from django.db.models import Q +from django.db import migrations + +def convert_review_rating(app_registry, schema_editor): + ''' take reviews with no content and turn them into ratings ''' + db_alias = schema_editor.connection.alias + reviews = app_registry.get_model('bookwyrm', 'Review') + review_ratings = app_registry.get_model('bookwyrm', 'ReviewRating') + ratings = reviews.objects.using(db_alias).filter( + Q(content__isnull=True) | Q(content='')) + # replace the old review with the rating + for review in ratings: + rating = review_ratings.objects.create( + user=review.user, rating=review.rating, book=review.book) + print('-----') + print(rating, review) + for field in review._meta.get_fields(): + if isinstance(field, ManyToOneRel) or field.name in ['status_ptr', 'id', 'remote_id']: + continue + value = getattr(review, field.name) + try: + setattr(rating, field.name, value) + except TypeError: + getattr(rating, field.name).set(value.all()) + rating.save() + + # move related models away from old review + related_models = [ + (r.remote_field.name, r.related_model.__name__) for r in \ + review._meta.related_objects] + for (related_field, related_model_name) in related_models: + related_model = app_registry.get_model('bookwyrm', related_model_name) + related_objs = related_model.objects.using(db_alias).filter( + **{related_field: review}) + for related_obj in related_objs: + print('related_obj', related_obj) + print('field', related_field) + try: + print(rating) + print('before', getattr(related_obj, related_field)) + setattr(related_obj, related_field, rating) + related_obj.save() + print('after', getattr(related_obj, related_field)) + except TypeError: + getattr(related_obj, related_field).add(rating) + getattr(related_obj, related_field).remove(review) + review.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0030_reviewrating'), + ] + + operations = [ + migrations.RunPython(convert_review_rating), + ] From ff9caf3d5118c7f575287af9121fe236a67a7027 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 4 Jan 2021 21:37:09 -0800 Subject: [PATCH 003/145] Fixes migration version numbering --- bookwyrm/migrations/0030_reviewrating.py | 23 ------------------- ...to_20210101_2109.py => 0033_reviewrate.py} | 17 +++++++++++--- 2 files changed, 14 insertions(+), 26 deletions(-) delete mode 100644 bookwyrm/migrations/0030_reviewrating.py rename bookwyrm/migrations/{0031_auto_20210101_2109.py => 0033_reviewrate.py} (80%) diff --git a/bookwyrm/migrations/0030_reviewrating.py b/bookwyrm/migrations/0030_reviewrating.py deleted file mode 100644 index 327c6c6a8..000000000 --- a/bookwyrm/migrations/0030_reviewrating.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.0.7 on 2021-01-01 19:05 - -from django.db import migrations, models -import django.db.models.deletion - -class Migration(migrations.Migration): - - dependencies = [ - ('bookwyrm', '0029_auto_20201221_2014'), - ] - - operations = [ - migrations.CreateModel( - name='ReviewRating', - fields=[ - ('review_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Review')), - ], - options={ - 'abstract': False, - }, - bases=('bookwyrm.review',), - ), - ] diff --git a/bookwyrm/migrations/0031_auto_20210101_2109.py b/bookwyrm/migrations/0033_reviewrate.py similarity index 80% rename from bookwyrm/migrations/0031_auto_20210101_2109.py rename to bookwyrm/migrations/0033_reviewrate.py index 8343719f7..69c0ec578 100644 --- a/bookwyrm/migrations/0031_auto_20210101_2109.py +++ b/bookwyrm/migrations/0033_reviewrate.py @@ -1,8 +1,9 @@ -# Generated by Django 3.0.7 on 2021-01-01 21:09 +# Generated by Django 3.0.7 on 2021-01-05 05:32 from django.db.models.fields.reverse_related import ManyToOneRel from django.db.models import Q -from django.db import migrations +from django.db import migrations, models +import django.db.models.deletion def convert_review_rating(app_registry, schema_editor): ''' take reviews with no content and turn them into ratings ''' @@ -53,9 +54,19 @@ def convert_review_rating(app_registry, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0030_reviewrating'), + ('bookwyrm', '0032_auto_20210104_2055'), ] operations = [ + migrations.CreateModel( + name='ReviewRating', + fields=[ + ('review_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Review')), + ], + options={ + 'abstract': False, + }, + bases=('bookwyrm.review',), + ), migrations.RunPython(convert_review_rating), ] From 252f09325a9b3622cfade6acb8db44fdb951b7f1 Mon Sep 17 00:00:00 2001 From: Jim Fingal Date: Sun, 21 Feb 2021 23:07:58 -0800 Subject: [PATCH 004/145] Add black command --- Makefile | 5 +++++ requirements.txt | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..44abad0b3 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +.PHONY: itblack + +itblack: + docker-compose run --rm web black celerywyrm + docker-compose run --rm web black bookwyrm \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e5d7798d7..f354fd439 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ celery==4.4.2 -coverage==5.1 Django==3.0.7 django-model-utils==4.0.0 environs==7.2.0 @@ -8,11 +7,15 @@ Markdown==3.3.3 Pillow>=7.1.0 psycopg2==2.8.4 pycryptodome==3.9.4 -pytest-django==4.1.0 -pytest==6.1.2 -pytest-cov==2.10.1 python-dateutil==2.8.1 redis==3.4.1 requests==2.22.0 responses==0.10.14 django-rename-app==0.1.2 + +# Dev +black==20.8b1 +coverage==5.1 +pytest-django==4.1.0 +pytest==6.1.2 +pytest-cov==2.10.1 From 6d7e063b81874c9a797afb5571c3224d4a79ad17 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 25 Feb 2021 10:17:24 -0800 Subject: [PATCH 005/145] Makes rating field mandatory on ratings --- bookwyrm/activitypub/note.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index ddb3f7ab9..ad08e3241 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -67,7 +67,7 @@ class Review(Comment): @dataclass(init=False) class Rating(Comment): - ''' a full book review ''' - rating: int = None + ''' just a star rating ''' + rating: int content: str = None type: str = 'Rating' From ffd57dfef48e6dd92ab3c43abd5bac51ce8dc2d7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 25 Feb 2021 10:26:24 -0800 Subject: [PATCH 006/145] Use modern string formatting syntax in status model --- bookwyrm/models/status.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 16b32891a..62ce5f1c0 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -258,13 +258,12 @@ class Review(Status): def pure_name(self): ''' clarify review names for mastodon serialization ''' if self.rating: - #pylint: disable=bad-string-format-type - return 'Review of "%s" (%d stars): %s' % ( + return 'Review of "{}" ({:d} stars): {}'.format( self.book.title, self.rating, self.name ) - return 'Review of "%s": %s' % ( + return 'Review of "{}": {}'.format( self.book.title, self.name ) @@ -282,13 +281,13 @@ class ReviewRating(Review): ''' a subtype of review that only contains a rating ''' def save(self, *args, **kwargs): if not self.rating: - raise ValueError('Rating object must include a numerical rating') + raise ValueError( + 'ReviewRating object must include a numerical rating') return super().save(*args, **kwargs) @property def pure_content(self): - #pylint: disable=bad-string-format-type - return 'Rated "%s": %d stars' % (self.book.title, self.rating) + return 'Rated "{}": {:d} stars'.format(self.book.title, self.rating) activity_serializer = activitypub.Rating pure_type = 'Note' From 2e36cfbcf64ec62d5ee10253fb8530df63f598f0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 25 Feb 2021 14:58:09 -0800 Subject: [PATCH 007/145] Creates new semi-functional migration --- bookwyrm/migrations/0033_reviewrate.py | 72 ------------------------ bookwyrm/migrations/0046_reviewrating.py | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+), 72 deletions(-) delete mode 100644 bookwyrm/migrations/0033_reviewrate.py create mode 100644 bookwyrm/migrations/0046_reviewrating.py diff --git a/bookwyrm/migrations/0033_reviewrate.py b/bookwyrm/migrations/0033_reviewrate.py deleted file mode 100644 index 69c0ec578..000000000 --- a/bookwyrm/migrations/0033_reviewrate.py +++ /dev/null @@ -1,72 +0,0 @@ -# Generated by Django 3.0.7 on 2021-01-05 05:32 - -from django.db.models.fields.reverse_related import ManyToOneRel -from django.db.models import Q -from django.db import migrations, models -import django.db.models.deletion - -def convert_review_rating(app_registry, schema_editor): - ''' take reviews with no content and turn them into ratings ''' - db_alias = schema_editor.connection.alias - reviews = app_registry.get_model('bookwyrm', 'Review') - review_ratings = app_registry.get_model('bookwyrm', 'ReviewRating') - ratings = reviews.objects.using(db_alias).filter( - Q(content__isnull=True) | Q(content='')) - # replace the old review with the rating - for review in ratings: - rating = review_ratings.objects.create( - user=review.user, rating=review.rating, book=review.book) - print('-----') - print(rating, review) - for field in review._meta.get_fields(): - if isinstance(field, ManyToOneRel) or field.name in ['status_ptr', 'id', 'remote_id']: - continue - value = getattr(review, field.name) - try: - setattr(rating, field.name, value) - except TypeError: - getattr(rating, field.name).set(value.all()) - rating.save() - - # move related models away from old review - related_models = [ - (r.remote_field.name, r.related_model.__name__) for r in \ - review._meta.related_objects] - for (related_field, related_model_name) in related_models: - related_model = app_registry.get_model('bookwyrm', related_model_name) - related_objs = related_model.objects.using(db_alias).filter( - **{related_field: review}) - for related_obj in related_objs: - print('related_obj', related_obj) - print('field', related_field) - try: - print(rating) - print('before', getattr(related_obj, related_field)) - setattr(related_obj, related_field, rating) - related_obj.save() - print('after', getattr(related_obj, related_field)) - except TypeError: - getattr(related_obj, related_field).add(rating) - getattr(related_obj, related_field).remove(review) - review.delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('bookwyrm', '0032_auto_20210104_2055'), - ] - - operations = [ - migrations.CreateModel( - name='ReviewRating', - fields=[ - ('review_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Review')), - ], - options={ - 'abstract': False, - }, - bases=('bookwyrm.review',), - ), - migrations.RunPython(convert_review_rating), - ] diff --git a/bookwyrm/migrations/0046_reviewrating.py b/bookwyrm/migrations/0046_reviewrating.py new file mode 100644 index 000000000..b29cdbd29 --- /dev/null +++ b/bookwyrm/migrations/0046_reviewrating.py @@ -0,0 +1,59 @@ +# Generated by Django 3.0.7 on 2021-02-25 18:36 + +from django.db import migrations, models +from django.db import connection +from django.db.models import Q +import django.db.models.deletion + +def convert_review_rating(app_registry, schema_editor): + ''' take rating type Reviews and conver them to ReviewRatings ''' + db_alias = schema_editor.connection.alias + + reviews = app_registry.get_model( + 'bookwyrm', 'Review' + ).objects.using(db_alias).filter( + Q(content__isnull=True) | Q(content='') + ) + + with connection.cursor() as cursor: + for review in reviews: + cursor.execute(''' +INSERT INTO bookwyrm_reviewrating(review_ptr_id) +SELECT status_ptr_id FROM bookwyrm_review +WHERE status_ptr_id={:d}'''.format(review.id)) + +def unconvert_review_rating(app_registry, schema_editor): + ''' undo the conversion from ratings back to reviews''' + # TODO: this does not work + db_alias = schema_editor.connection.alias + + ratings = app_registry.get_model( + 'bookwyrm', 'ReviewRating' + ).objects.using(db_alias).all() + + with connection.cursor() as cursor: + for rating in ratings: + cursor.execute(''' +INSERT INTO bookwyrm_review(status_ptr_id) +SELECT review_ptr_id FROM bookwyrm_reviewrating +WHERE review_ptr_id={:d}'''.format(rating.id)) + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0045_auto_20210210_2114'), + ] + + operations = [ + migrations.CreateModel( + name='ReviewRating', + fields=[ + ('review_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Review')), + ], + options={ + 'abstract': False, + }, + bases=('bookwyrm.review',), + ), + migrations.RunPython(convert_review_rating, unconvert_review_rating), + ] From 4a10c99026475e6dc79ba40d24dd4a98491bc076 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 25 Feb 2021 15:05:51 -0800 Subject: [PATCH 008/145] Fixes handling of ratings in status views and urls --- bookwyrm/templates/snippets/status/status_content.html | 2 +- bookwyrm/templates/snippets/status/status_header.html | 2 +- bookwyrm/urls.py | 1 + bookwyrm/views/status.py | 4 +++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/snippets/status/status_content.html b/bookwyrm/templates/snippets/status/status_content.html index 0f59f7fcb..e493e4bfd 100644 --- a/bookwyrm/templates/snippets/status/status_content.html +++ b/bookwyrm/templates/snippets/status/status_content.html @@ -1,6 +1,6 @@ {% load bookwyrm_tags %}
- {% if status.status_type == 'Review' %} + {% if status.status_type == 'Review' or status.status_type == 'Rating' %}

{% if status.name %}{{ status.name }}
{% endif %} diff --git a/bookwyrm/templates/snippets/status/status_header.html b/bookwyrm/templates/snippets/status/status_header.html index 890b4ab86..f2e897ccb 100644 --- a/bookwyrm/templates/snippets/status/status_header.html +++ b/bookwyrm/templates/snippets/status/status_header.html @@ -4,7 +4,7 @@ {% if status.status_type == 'GeneratedNote' %} {{ status.content | safe }} -{% elif status.status_type == 'Review' and not status.name and not status.content%} +{% elif status.status_type == 'Rating' %} rated {% elif status.status_type == 'Review' %} reviewed diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index a741088a2..76054e3fd 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -13,6 +13,7 @@ local_user_path = r'^user/(?P%s)' % regex.localname status_types = [ 'status', 'review', + 'reviewrating', 'comment', 'quotation', 'boost', diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py index db924ce8b..983fea382 100644 --- a/bookwyrm/views/status.py +++ b/bookwyrm/views/status.py @@ -56,7 +56,7 @@ class CreateStatus(View): status.mention_users.set(set(status.mention_users.all())) # don't apply formatting to generated notes - if not isinstance(status, models.GeneratedNote): + if not isinstance(status, models.GeneratedNote) and content: status.content = to_markdown(content) # do apply formatting to quotes if hasattr(status, 'quote'): @@ -82,6 +82,8 @@ class DeleteStatus(View): def find_mentions(content): ''' detect @mentions in raw status content ''' + if not content: + return for match in re.finditer(regex.strict_username, content): username = match.group().strip().split('@')[1:] if len(username) == 1: From 3ba6479e79ad5280273aa57aca6c9a6535099de4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 25 Feb 2021 15:16:16 -0800 Subject: [PATCH 009/145] hide linter error on override funtion --- bookwyrm/activitypub/note.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 54c69ce44..452184217 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -11,7 +11,7 @@ class Tombstone(ActivityObject): ''' the placeholder for a deleted status ''' type: str = 'Tombstone' - def to_model(self, *args, **kwargs): + def to_model(self, *args, **kwargs):# pylint: disable=unused-argument ''' this should never really get serialized, just searched for ''' model = apps.get_model('bookwyrm.Status') return model.find_existing_by_remote_id(self.id) From ee7388052c45f521cdff5d00c63ab6b07d43b2c1 Mon Sep 17 00:00:00 2001 From: Joel Bradshaw Date: Fri, 26 Feb 2021 22:57:26 -0800 Subject: [PATCH 010/145] Use SQL parameters, and make unconvert work DBAs don't want you to know about this One Simple Trick --- bookwyrm/migrations/0046_reviewrating.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/bookwyrm/migrations/0046_reviewrating.py b/bookwyrm/migrations/0046_reviewrating.py index b29cdbd29..776cae841 100644 --- a/bookwyrm/migrations/0046_reviewrating.py +++ b/bookwyrm/migrations/0046_reviewrating.py @@ -6,7 +6,7 @@ from django.db.models import Q import django.db.models.deletion def convert_review_rating(app_registry, schema_editor): - ''' take rating type Reviews and conver them to ReviewRatings ''' + ''' take rating type Reviews and convert them to ReviewRatings ''' db_alias = schema_editor.connection.alias reviews = app_registry.get_model( @@ -20,23 +20,13 @@ def convert_review_rating(app_registry, schema_editor): cursor.execute(''' INSERT INTO bookwyrm_reviewrating(review_ptr_id) SELECT status_ptr_id FROM bookwyrm_review -WHERE status_ptr_id={:d}'''.format(review.id)) +WHERE status_ptr_id=%s''', (review.id)) def unconvert_review_rating(app_registry, schema_editor): ''' undo the conversion from ratings back to reviews''' - # TODO: this does not work - db_alias = schema_editor.connection.alias - - ratings = app_registry.get_model( - 'bookwyrm', 'ReviewRating' - ).objects.using(db_alias).all() - - with connection.cursor() as cursor: - for rating in ratings: - cursor.execute(''' -INSERT INTO bookwyrm_review(status_ptr_id) -SELECT review_ptr_id FROM bookwyrm_reviewrating -WHERE review_ptr_id={:d}'''.format(rating.id)) + # All we need to do to revert this is drop the table, which Django will do + # on its own, as long as we have a valid reverse function. So, this is a + # no-op function so Django will do its thing class Migration(migrations.Migration): From 717cbe303412e40feb4cf3553ed0cc760eec581c Mon Sep 17 00:00:00 2001 From: Joel Bradshaw Date: Fri, 26 Feb 2021 23:12:39 -0800 Subject: [PATCH 011/145] Use very fancy SQL nonsense This should be more efficient than running the queries one by one --- bookwyrm/migrations/0046_reviewrating.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bookwyrm/migrations/0046_reviewrating.py b/bookwyrm/migrations/0046_reviewrating.py index 776cae841..e410ba91f 100644 --- a/bookwyrm/migrations/0046_reviewrating.py +++ b/bookwyrm/migrations/0046_reviewrating.py @@ -4,6 +4,7 @@ from django.db import migrations, models from django.db import connection from django.db.models import Q import django.db.models.deletion +from psycopg2.extras import execute_values def convert_review_rating(app_registry, schema_editor): ''' take rating type Reviews and convert them to ReviewRatings ''' @@ -16,11 +17,10 @@ def convert_review_rating(app_registry, schema_editor): ) with connection.cursor() as cursor: - for review in reviews: - cursor.execute(''' + values = [(r.id,) for r in reviews] + execute_values(cursor, ''' INSERT INTO bookwyrm_reviewrating(review_ptr_id) -SELECT status_ptr_id FROM bookwyrm_review -WHERE status_ptr_id=%s''', (review.id)) +VALUES %s''', values) def unconvert_review_rating(app_registry, schema_editor): ''' undo the conversion from ratings back to reviews''' From 0cf2baccac45c92e7c4cb706f9b452575961e2d7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 28 Feb 2021 10:40:10 -0800 Subject: [PATCH 012/145] Adds merge migration --- bookwyrm/migrations/0047_merge_20210228_1839.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 bookwyrm/migrations/0047_merge_20210228_1839.py diff --git a/bookwyrm/migrations/0047_merge_20210228_1839.py b/bookwyrm/migrations/0047_merge_20210228_1839.py new file mode 100644 index 000000000..7e410761a --- /dev/null +++ b/bookwyrm/migrations/0047_merge_20210228_1839.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.7 on 2021-02-28 18:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0046_reviewrating'), + ('bookwyrm', '0046_sitesettings_privacy_policy'), + ] + + operations = [ + ] From 2faf5cea2a723c43b09b5ffba164e951b23f9682 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 2 Mar 2021 09:01:31 -0800 Subject: [PATCH 013/145] 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 4d2159497..aab9e1035 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 a741088a2..dfb64c234 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 4d6afba96..1754982e8 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 014/145] 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 aab9e1035..d0eb50bec 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 b3360200d..b7eaac4bc 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 1754982e8..174ddaa30 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 015/145] 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 d0eb50bec..0637bb79c 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 b7eaac4bc..8a6c66cb8 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 dfb64c234..9e0d46300 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 2c7cdc461..c3d17da76 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 174ddaa30..0097eb9f6 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 9536f0058af4eaebe8b9aa51c910c70164f2edaf Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 6 Mar 2021 13:43:20 -0800 Subject: [PATCH 016/145] Testing out a Black github action --- .github/workflows/black.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/black.yml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 000000000..de770ccee --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,13 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: psf/black@stable + with: + args: ". --check -l 80 -S" From bbd3ac7242662964b055c89344de25c745c77019 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 7 Mar 2021 13:17:11 -0800 Subject: [PATCH 017/145] 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 0637bb79c..dc927e8ab 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 018/145] 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 8a6c66cb8..0ba09e3b9 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 019/145] 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 dc927e8ab..a4d62efda 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 0097eb9f6..cc05a79c3 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 020/145] 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 0ba09e3b9..6a28ba9d6 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 cc05a79c3..07a1d4372 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 0146cebb1a961fdf187d301bd2e9f9dfa621dd40 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 7 Mar 2021 15:17:51 -0800 Subject: [PATCH 021/145] Fixes incorrect user displayed for boosts Fixes #706 --- bookwyrm/templates/snippets/status/status.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/snippets/status/status.html b/bookwyrm/templates/snippets/status/status.html index 50d3414dd..08898326c 100644 --- a/bookwyrm/templates/snippets/status/status.html +++ b/bookwyrm/templates/snippets/status/status.html @@ -4,7 +4,7 @@ {% if status.status_type == 'Announce' %} {% include 'snippets/avatar.html' with user=status.user %} - {{ user.display_name }} + {{ stauts.user.display_name }} {% trans "boosted" %} {% include 'snippets/status/status_body.html' with status=status|boosted_status %} From 7d55629823fc09771fa6340990d788b2dcc18ec2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 7 Mar 2021 15:20:09 -0800 Subject: [PATCH 022/145] typo fix --- bookwyrm/templates/snippets/status/status.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/snippets/status/status.html b/bookwyrm/templates/snippets/status/status.html index 08898326c..10492c48b 100644 --- a/bookwyrm/templates/snippets/status/status.html +++ b/bookwyrm/templates/snippets/status/status.html @@ -4,7 +4,7 @@ {% if status.status_type == 'Announce' %} {% include 'snippets/avatar.html' with user=status.user %} - {{ stauts.user.display_name }} + {{ status.user.display_name }} {% trans "boosted" %} {% include 'snippets/status/status_body.html' with status=status|boosted_status %} From 642a2f26d784fa49b131853016d456f11427baa5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 7 Mar 2021 15:42:49 -0800 Subject: [PATCH 023/145] Fixes support link display i18n --- bookwyrm/templates/layout.html | 2 +- locale/de_DE/LC_MESSAGES/django.po | 8 ++++++-- locale/en_US/LC_MESSAGES/django.po | 4 ++-- locale/es/LC_MESSAGES/django.po | 4 ++-- locale/fr_FR/LC_MESSAGES/django.po | 4 ++-- locale/zh_CN/LC_MESSAGES/django.po | 4 ++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 377acb6c5..901a12ff5 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -199,7 +199,7 @@ {% if site.support_link %}
- {% blocktrans %}Support {{ site.name }} on {{ site.support_title }}{% endblocktrans %} + {% blocktrans with site_name=site.name support_link=site.support_link support_title=site.support_title %}Support {{ site_name }} on {{ support_title }}{% endblocktrans %}
{% endif %}
diff --git a/locale/de_DE/LC_MESSAGES/django.po b/locale/de_DE/LC_MESSAGES/django.po index 44f51f47a..7eb5aa201 100644 --- a/locale/de_DE/LC_MESSAGES/django.po +++ b/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-03-07 18:25+0000\n" +"POT-Creation-Date: 2021-03-07 23:40+0000\n" "PO-Revision-Date: 2021-03-02 17:19-0800\n" "Last-Translator: Mouse Reeve \n" "Language-Team: English \n" @@ -129,10 +129,14 @@ msgid "ASIN:" msgstr "" #: bookwyrm/templates/book.html:84 +#, fuzzy, python-format +#| msgid "%(format)s, %(pages)s pages" msgid "%(format)s, %(pages)s pages" msgstr "%(format)s, %(book.pages)s Seiten" #: bookwyrm/templates/book.html:86 +#, fuzzy, python-format +#| msgid "%(pages)s pages" msgid "%(pages)s pages" msgstr "%(book.pages)s Seiten" @@ -750,7 +754,7 @@ msgstr "Admin kontaktieren" #: bookwyrm/templates/layout.html:202 #, python-format -msgid "Support %(site.name)s on %(site.support_title)s" +msgid "Support %(site_name)s on %(support_title)s" msgstr "" #: bookwyrm/templates/layout.html:206 diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 081d38dfe..d69fe7023 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-03-07 18:25+0000\n" +"POT-Creation-Date: 2021-03-07 23:40+0000\n" "PO-Revision-Date: 2021-02-28 17:19-0800\n" "Last-Translator: Mouse Reeve \n" "Language-Team: English \n" @@ -743,7 +743,7 @@ msgstr "" #: bookwyrm/templates/layout.html:202 #, python-format -msgid "Support %(site.name)s on %(site.support_title)s" +msgid "Support %(site_name)s on %(support_title)s" msgstr "" #: bookwyrm/templates/layout.html:206 diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 291304215..6c6ee4341 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-03-07 18:25+0000\n" +"POT-Creation-Date: 2021-03-07 23:40+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -743,7 +743,7 @@ msgstr "Contactarse con administradores del sitio" #: bookwyrm/templates/layout.html:202 #, python-format -msgid "Support %(site.name)s on %(site.support_title)s" +msgid "Support %(site_name)s on %(support_title)s" msgstr "" #: bookwyrm/templates/layout.html:206 diff --git a/locale/fr_FR/LC_MESSAGES/django.po b/locale/fr_FR/LC_MESSAGES/django.po index 63dd93a14..38cdd9557 100644 --- a/locale/fr_FR/LC_MESSAGES/django.po +++ b/locale/fr_FR/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-03-07 18:25+0000\n" +"POT-Creation-Date: 2021-03-07 23:40+0000\n" "PO-Revision-Date: 2021-03-02 12:37+0100\n" "Last-Translator: Fabien Basmaison \n" "Language-Team: Mouse Reeve \n" @@ -774,7 +774,7 @@ msgstr "Contacter l’administrateur du site" #: bookwyrm/templates/layout.html:202 #, python-format -msgid "Support %(site.name)s on %(site.support_title)s" +msgid "Support %(site_name)s on %(support_title)s" msgstr "" #: bookwyrm/templates/layout.html:206 diff --git a/locale/zh_CN/LC_MESSAGES/django.po b/locale/zh_CN/LC_MESSAGES/django.po index a0ac2639f..34dbed2a5 100644 --- a/locale/zh_CN/LC_MESSAGES/django.po +++ b/locale/zh_CN/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-03-07 18:25+0000\n" +"POT-Creation-Date: 2021-03-07 23:40+0000\n" "PO-Revision-Date: 2021-03-02 10:35+0000\n" "Last-Translator: Kana \n" "Language-Team: Mouse Reeve \n" @@ -753,7 +753,7 @@ msgstr "联系站点管理员" #: bookwyrm/templates/layout.html:202 #, python-format -msgid "Support %(site.name)s on %(site.support_title)s" +msgid "Support %(site_name)s on %(support_title)s" msgstr "" #: bookwyrm/templates/layout.html:206 From 33cf03df74409b7450304f1b455b9a1ed5b63164 Mon Sep 17 00:00:00 2001 From: Jim Fingal Date: Sun, 7 Mar 2021 19:43:36 -0800 Subject: [PATCH 024/145] Add black command to bw-dev --- Makefile | 5 ----- bw-dev | 9 ++++++++- 2 files changed, 8 insertions(+), 6 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 44abad0b3..000000000 --- a/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -.PHONY: itblack - -itblack: - docker-compose run --rm web black celerywyrm - docker-compose run --rm web black bookwyrm \ No newline at end of file diff --git a/bw-dev b/bw-dev index f3162bd36..2aa469423 100755 --- a/bw-dev +++ b/bw-dev @@ -35,6 +35,10 @@ function initdb { execweb python manage.py initdb } +function makeitblack { + runweb black celerywyrm bookwyrm +} + CMD=$1 shift @@ -96,7 +100,10 @@ case "$CMD" in clean) clean ;; + black) + makeitblack + ;; *) - echo "Unrecognised command. Try: build, clean, up, initdb, resetdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, test, pytest, test_report" + echo "Unrecognised command. Try: build, clean, up, initdb, resetdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, test, pytest, test_report, black" ;; esac From 70296e760b7400762744a3c8ec18b841cb39a344 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 08:49:10 -0800 Subject: [PATCH 025/145] Runs black --- bookwyrm/activitypub/__init__.py | 8 +- bookwyrm/activitypub/base_activity.py | 128 +- bookwyrm/activitypub/book.py | 67 +- bookwyrm/activitypub/image.py | 12 +- bookwyrm/activitypub/note.py | 48 +- bookwyrm/activitypub/ordered_collection.py | 30 +- bookwyrm/activitypub/person.py | 12 +- bookwyrm/activitypub/response.py | 16 +- bookwyrm/activitypub/verbs.py | 101 +- bookwyrm/admin.py | 2 +- bookwyrm/connectors/__init__.py | 2 +- bookwyrm/connectors/abstract_connector.py | 125 +- bookwyrm/connectors/bookwyrm_connector.py | 9 +- bookwyrm/connectors/connector_manager.py | 59 +- bookwyrm/connectors/openlibrary.py | 205 +-- bookwyrm/connectors/openlibrary_languages.py | 930 +++++----- bookwyrm/connectors/self_connector.py | 86 +- bookwyrm/connectors/settings.py | 4 +- bookwyrm/context_processors.py | 11 +- bookwyrm/emailing.py | 16 +- bookwyrm/forms.py | 175 +- bookwyrm/goodreads_import.py | 11 +- bookwyrm/importer.py | 82 +- bookwyrm/librarything_import.py | 46 +- .../commands/deduplicate_book_data.py | 58 +- bookwyrm/management/commands/initdb.py | 128 +- .../management/commands/remove_editions.py | 40 +- bookwyrm/migrations/0001_initial.py | 493 +++-- .../migrations/0002_auto_20200219_0816.py | 56 +- .../migrations/0003_auto_20200221_0131.py | 74 +- bookwyrm/migrations/0004_tag.py | 37 +- .../migrations/0005_auto_20200221_1645.py | 14 +- ..._1702_squashed_0064_merge_20201101_1913.py | 1625 +++++++++++------ .../migrations/0007_auto_20201103_0014.py | 10 +- .../migrations/0008_work_default_edition.py | 21 +- bookwyrm/migrations/0009_shelf_privacy.py | 17 +- bookwyrm/migrations/0010_importjob_retry.py | 6 +- .../migrations/0011_auto_20201113_1727.py | 13 +- bookwyrm/migrations/0012_attachment.py | 38 +- bookwyrm/migrations/0012_progressupdate.py | 49 +- bookwyrm/migrations/0013_book_origin_id.py | 6 +- .../migrations/0014_auto_20201128_0118.py | 6 +- .../migrations/0014_merge_20201128_0007.py | 7 +- .../migrations/0015_auto_20201128_0349.py | 13 +- .../migrations/0015_auto_20201128_0734.py | 16 +- .../migrations/0016_auto_20201129_0304.py | 95 +- .../migrations/0016_auto_20201211_2026.py | 14 +- .../migrations/0017_auto_20201130_1819.py | 302 ++- .../migrations/0017_auto_20201212_0059.py | 10 +- .../migrations/0018_auto_20201130_1832.py | 14 +- .../migrations/0019_auto_20201130_1939.py | 22 +- .../migrations/0020_auto_20201208_0213.py | 526 ++++-- .../migrations/0021_merge_20201212_1737.py | 7 +- .../migrations/0022_auto_20201212_1744.py | 15 +- .../migrations/0023_auto_20201214_0511.py | 17 +- .../migrations/0023_merge_20201216_0112.py | 7 +- .../migrations/0024_merge_20201216_1721.py | 7 +- .../migrations/0025_auto_20201217_0046.py | 24 +- .../migrations/0026_status_content_warning.py | 10 +- .../migrations/0027_auto_20201220_2007.py | 14 +- .../0028_remove_book_author_text.py | 6 +- .../migrations/0029_auto_20201221_2014.py | 62 +- .../migrations/0030_auto_20201224_1939.py | 13 +- .../migrations/0031_auto_20210104_2040.py | 20 +- .../migrations/0032_auto_20210104_2055.py | 16 +- .../0033_siteinvite_created_date.py | 10 +- .../migrations/0034_importjob_complete.py | 6 +- .../migrations/0035_edition_edition_rank.py | 9 +- bookwyrm/migrations/0036_annualgoal.py | 55 +- .../migrations/0037_auto_20210118_1954.py | 17 +- .../migrations/0038_auto_20210119_1534.py | 10 +- .../migrations/0039_merge_20210120_0753.py | 7 +- .../migrations/0040_auto_20210122_0057.py | 38 +- .../migrations/0041_auto_20210131_1614.py | 149 +- .../migrations/0042_auto_20210201_2108.py | 36 +- .../migrations/0043_auto_20210204_2223.py | 14 +- .../migrations/0044_auto_20210207_1924.py | 15 +- .../migrations/0045_auto_20210210_2114.py | 103 +- .../0046_sitesettings_privacy_policy.py | 8 +- .../0047_connector_isbn_search_url.py | 6 +- bookwyrm/models/__init__.py | 12 +- bookwyrm/models/activitypub_mixin.py | 309 ++-- bookwyrm/models/attachment.py | 22 +- bookwyrm/models/author.py | 12 +- bookwyrm/models/base_model.py | 30 +- bookwyrm/models/book.py | 156 +- bookwyrm/models/connector.py | 21 +- bookwyrm/models/favorite.py | 39 +- bookwyrm/models/federated_server.py | 8 +- bookwyrm/models/fields.py | 258 +-- bookwyrm/models/import_job.py | 134 +- bookwyrm/models/list.py | 83 +- bookwyrm/models/notification.py | 53 +- bookwyrm/models/readthrough.py | 55 +- bookwyrm/models/relationship.py | 136 +- bookwyrm/models/shelf.py | 59 +- bookwyrm/models/site.py | 69 +- bookwyrm/models/status.py | 230 +-- bookwyrm/models/tag.py | 47 +- bookwyrm/models/user.py | 290 +-- bookwyrm/sanitize_html.py | 51 +- bookwyrm/settings.py | 159 +- bookwyrm/signatures.py | 99 +- bookwyrm/status.py | 14 +- bookwyrm/tasks.py | 6 +- bookwyrm/templatetags/bookwyrm_tags.py | 224 ++- bookwyrm/tests/activitypub/test_author.py | 19 +- .../tests/activitypub/test_base_activity.py | 210 ++- bookwyrm/tests/activitypub/test_person.py | 18 +- bookwyrm/tests/activitypub/test_quotation.py | 47 +- .../connectors/test_abstract_connector.py | 96 +- .../test_abstract_minimal_connector.py | 110 +- .../connectors/test_bookwyrm_connector.py | 45 +- .../connectors/test_connector_manager.py | 71 +- .../connectors/test_openlibrary_connector.py | 246 ++- .../tests/connectors/test_self_connector.py | 86 +- .../tests/models/test_activitypub_mixin.py | 339 ++-- bookwyrm/tests/models/test_base_model.py | 27 +- bookwyrm/tests/models/test_book_model.py | 78 +- bookwyrm/tests/models/test_fields.py | 418 ++--- bookwyrm/tests/models/test_import_model.py | 187 +- bookwyrm/tests/models/test_list.py | 54 +- .../tests/models/test_readthrough_model.py | 31 +- .../tests/models/test_relationship_models.py | 101 +- bookwyrm/tests/models/test_shelf_model.py | 114 +- bookwyrm/tests/models/test_status_model.py | 371 ++-- bookwyrm/tests/models/test_user_model.py | 139 +- bookwyrm/tests/test_goodreads_import.py | 197 +- bookwyrm/tests/test_librarything_import.py | 197 +- bookwyrm/tests/test_sanitize_html.py | 32 +- bookwyrm/tests/test_signing.py | 165 +- bookwyrm/tests/test_templatetags.py | 274 ++- bookwyrm/tests/views/test_authentication.py | 155 +- bookwyrm/tests/views/test_author.py | 92 +- bookwyrm/tests/views/test_block.py | 50 +- bookwyrm/tests/views/test_book.py | 98 +- bookwyrm/tests/views/test_federation.py | 20 +- bookwyrm/tests/views/test_feed.py | 80 +- bookwyrm/tests/views/test_follow.py | 129 +- bookwyrm/tests/views/test_goal.py | 86 +- bookwyrm/tests/views/test_helpers.py | 324 ++-- bookwyrm/tests/views/test_import.py | 27 +- bookwyrm/tests/views/test_inbox.py | 636 +++---- bookwyrm/tests/views/test_interaction.py | 116 +- bookwyrm/tests/views/test_invite.py | 31 +- bookwyrm/tests/views/test_isbn.py | 45 +- bookwyrm/tests/views/test_landing.py | 30 +- bookwyrm/tests/views/test_list.py | 332 ++-- bookwyrm/tests/views/test_notifications.py | 29 +- bookwyrm/tests/views/test_outbox.py | 137 +- bookwyrm/tests/views/test_password.py | 84 +- bookwyrm/tests/views/test_reading.py | 162 +- bookwyrm/tests/views/test_readthrough.py | 78 +- bookwyrm/tests/views/test_rss_feed.py | 48 +- bookwyrm/tests/views/test_search.py | 98 +- bookwyrm/tests/views/test_shelf.py | 181 +- bookwyrm/tests/views/test_status.py | 294 +-- bookwyrm/tests/views/test_tag.py | 110 +- bookwyrm/tests/views/test_user.py | 163 +- bookwyrm/urls.py | 273 ++- bookwyrm/utils/regex.py | 16 +- bookwyrm/views/__init__.py | 2 +- bookwyrm/views/authentication.py | 86 +- bookwyrm/views/author.py | 45 +- bookwyrm/views/block.py | 22 +- bookwyrm/views/books.py | 135 +- bookwyrm/views/error.py | 11 +- bookwyrm/views/federation.py | 18 +- bookwyrm/views/feed.py | 145 +- bookwyrm/views/follow.py | 30 +- bookwyrm/views/goal.py | 50 +- bookwyrm/views/helpers.py | 110 +- bookwyrm/views/import_data.py | 68 +- bookwyrm/views/inbox.py | 30 +- bookwyrm/views/interaction.py | 53 +- bookwyrm/views/invite.py | 64 +- bookwyrm/views/isbn.py | 15 +- bookwyrm/views/landing.py | 50 +- bookwyrm/views/list.py | 145 +- bookwyrm/views/notifications.py | 22 +- bookwyrm/views/outbox.py | 11 +- bookwyrm/views/password.py | 68 +- bookwyrm/views/reading.py | 87 +- bookwyrm/views/rss_feed.py | 29 +- bookwyrm/views/search.py | 74 +- bookwyrm/views/shelf.py | 99 +- bookwyrm/views/site.py | 27 +- bookwyrm/views/status.py | 50 +- bookwyrm/views/tag.py | 45 +- bookwyrm/views/updates.py | 21 +- bookwyrm/views/user.py | 129 +- bookwyrm/wellknown.py | 145 +- celerywyrm/__init__.py | 4 +- celerywyrm/asgi.py | 2 +- celerywyrm/celery.py | 23 +- celerywyrm/settings.py | 124 +- celerywyrm/urls.py | 2 +- celerywyrm/wsgi.py | 2 +- 198 files changed, 10239 insertions(+), 8572 deletions(-) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index fdfbb1f06..1a156cae6 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -1,4 +1,4 @@ -''' bring activitypub functions into the namespace ''' +""" bring activitypub functions into the namespace """ import inspect import sys @@ -21,9 +21,9 @@ from .verbs import Announce, Like # this creates a list of all the Activity types that we can serialize, # so when an Activity comes in from outside, we can check if it's known cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) -activity_objects = {c[0]: c[1] for c in cls_members \ - if hasattr(c[1], 'to_model')} +activity_objects = {c[0]: c[1] for c in cls_members if hasattr(c[1], "to_model")} + def parse(activity_json): - ''' figure out what activity this is and parse it ''' + """ figure out what activity this is and parse it """ return naive_parse(activity_objects, activity_json) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index c732fe1d3..315ff58c8 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -1,4 +1,4 @@ -''' basics for an activitypub serializer ''' +""" basics for an activitypub serializer """ from dataclasses import dataclass, fields, MISSING from json import JSONEncoder @@ -8,46 +8,52 @@ from django.db import IntegrityError, transaction from bookwyrm.connectors import ConnectorException, get_data from bookwyrm.tasks import app + class ActivitySerializerError(ValueError): - ''' routine problems serializing activitypub json ''' + """ routine problems serializing activitypub json """ class ActivityEncoder(JSONEncoder): - ''' used to convert an Activity object into json ''' + """ used to convert an Activity object into json """ + def default(self, o): return o.__dict__ @dataclass class Link: - ''' for tagging a book in a status ''' + """ for tagging a book in a status """ + href: str name: str - type: str = 'Link' + type: str = "Link" @dataclass class Mention(Link): - ''' a subtype of Link for mentioning an actor ''' - type: str = 'Mention' + """ a subtype of Link for mentioning an actor """ + + type: str = "Mention" @dataclass class Signature: - ''' public key block ''' + """ public key block """ + creator: str created: str signatureValue: str - type: str = 'RsaSignature2017' + type: str = "RsaSignature2017" + def naive_parse(activity_objects, activity_json, serializer=None): - ''' this navigates circular import issues ''' + """ this navigates circular import issues """ if not serializer: - if activity_json.get('publicKeyPem'): + if activity_json.get("publicKeyPem"): # ugh - activity_json['type'] = 'PublicKey' + activity_json["type"] = "PublicKey" try: - activity_type = activity_json['type'] + activity_type = activity_json["type"] serializer = activity_objects[activity_type] except KeyError as e: raise ActivitySerializerError(e) @@ -57,14 +63,15 @@ def naive_parse(activity_objects, activity_json, serializer=None): @dataclass(init=False) class ActivityObject: - ''' actor activitypub json ''' + """ actor activitypub json """ + id: str type: str def __init__(self, activity_objects=None, **kwargs): - ''' this lets you pass in an object with fields that aren't in the + """this lets you pass in an object with fields that aren't in the dataclass, which it ignores. Any field in the dataclass is required or - has a default value ''' + has a default value""" for field in fields(self): try: value = kwargs[field.name] @@ -75,7 +82,7 @@ class ActivityObject: except TypeError: is_subclass = False # serialize a model obj - if hasattr(value, 'to_activity'): + if hasattr(value, "to_activity"): value = value.to_activity() # parse a dict into the appropriate activity elif is_subclass and isinstance(value, dict): @@ -83,25 +90,27 @@ class ActivityObject: value = naive_parse(activity_objects, value) else: value = naive_parse( - activity_objects, value, serializer=field.type) + activity_objects, value, serializer=field.type + ) except KeyError: - if field.default == MISSING and \ - field.default_factory == MISSING: - raise ActivitySerializerError(\ - 'Missing required field: %s' % field.name) + if field.default == MISSING and field.default_factory == MISSING: + raise ActivitySerializerError( + "Missing required field: %s" % field.name + ) value = field.default setattr(self, field.name, value) - def to_model(self, model=None, instance=None, allow_create=True, save=True): - ''' convert from an activity to a model instance ''' + """ convert from an activity to a model instance """ model = model or get_model_from_type(self.type) # only reject statuses if we're potentially creating them - if allow_create and \ - hasattr(model, 'ignore_activity') and \ - model.ignore_activity(self): + if ( + allow_create + and hasattr(model, "ignore_activity") + and model.ignore_activity(self) + ): raise ActivitySerializerError() # check for an existing instance @@ -142,8 +151,10 @@ class ActivityObject: field.set_field_from_activity(instance, self) # reversed relationships in the models - for (model_field_name, activity_field_name) in \ - instance.deserialize_reverse_fields: + for ( + model_field_name, + activity_field_name, + ) in instance.deserialize_reverse_fields: # attachments on Status, for example values = getattr(self, activity_field_name) if values is None or values is MISSING: @@ -161,13 +172,12 @@ class ActivityObject: instance.__class__.__name__, related_field_name, instance.remote_id, - item + item, ) return instance - def serialize(self): - ''' convert to dictionary with context attr ''' + """ convert to dictionary with context attr """ data = self.__dict__.copy() # recursively serialize for (k, v) in data.items(): @@ -176,22 +186,19 @@ class ActivityObject: data[k] = v.serialize() except TypeError: pass - data = {k:v for (k, v) in data.items() if v is not None} - data['@context'] = 'https://www.w3.org/ns/activitystreams' + data = {k: v for (k, v) in data.items() if v is not None} + data["@context"] = "https://www.w3.org/ns/activitystreams" return data @app.task @transaction.atomic def set_related_field( - model_name, origin_model_name, related_field_name, - related_remote_id, data): - ''' load reverse related fields (editions, attachments) without blocking ''' - model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True) - origin_model = apps.get_model( - 'bookwyrm.%s' % origin_model_name, - require_ready=True - ) + model_name, origin_model_name, related_field_name, related_remote_id, data +): + """ load reverse related fields (editions, attachments) without blocking """ + model = apps.get_model("bookwyrm.%s" % model_name, require_ready=True) + origin_model = apps.get_model("bookwyrm.%s" % origin_model_name, require_ready=True) with transaction.atomic(): if isinstance(data, str): @@ -205,43 +212,45 @@ def set_related_field( # this must exist because it's the object that triggered this function instance = origin_model.find_existing_by_remote_id(related_remote_id) if not instance: - raise ValueError( - 'Invalid related remote id: %s' % related_remote_id) + raise ValueError("Invalid related remote id: %s" % related_remote_id) # set the origin's remote id on the activity so it will be there when # the model instance is created # edition.parentWork = instance, for example model_field = getattr(model, related_field_name) - if hasattr(model_field, 'activitypub_field'): + if hasattr(model_field, "activitypub_field"): setattr( - activity, - getattr(model_field, 'activitypub_field'), - instance.remote_id + activity, getattr(model_field, "activitypub_field"), instance.remote_id ) item = activity.to_model() # if the related field isn't serialized (attachments on Status), then # we have to set it post-creation - if not hasattr(model_field, 'activitypub_field'): + if not hasattr(model_field, "activitypub_field"): setattr(item, related_field_name, instance) item.save() def get_model_from_type(activity_type): - ''' given the activity, what type of model ''' + """ given the activity, what type of model """ models = apps.get_models() - model = [m for m in models if hasattr(m, 'activity_serializer') and \ - hasattr(m.activity_serializer, 'type') and \ - m.activity_serializer.type == activity_type] + model = [ + m + for m in models + if hasattr(m, "activity_serializer") + and hasattr(m.activity_serializer, "type") + and m.activity_serializer.type == activity_type + ] if not model: raise ActivitySerializerError( - 'No model found for activity type "%s"' % activity_type) + 'No model found for activity type "%s"' % activity_type + ) return model[0] def resolve_remote_id(remote_id, model=None, refresh=False, save=True): - ''' take a remote_id and return an instance, creating if necessary ''' - if model:# a bonus check we can do if we already know the model + """ take a remote_id and return an instance, creating if necessary """ + if model: # a bonus check we can do if we already know the model result = model.find_existing_by_remote_id(remote_id) if result and not refresh: return result @@ -251,11 +260,12 @@ def resolve_remote_id(remote_id, model=None, refresh=False, save=True): data = get_data(remote_id) except (ConnectorException, ConnectionError): raise ActivitySerializerError( - 'Could not connect to host for remote_id in %s model: %s' % \ - (model.__name__, remote_id)) + "Could not connect to host for remote_id in %s model: %s" + % (model.__name__, remote_id) + ) # determine the model implicitly, if not provided if not model: - model = get_model_from_type(data.get('type')) + model = get_model_from_type(data.get("type")) # check for existing items with shared unique identifiers result = model.find_existing(data) diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 8c32be967..7e552b0a8 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -1,70 +1,75 @@ -''' book and author data ''' +""" book and author data """ from dataclasses import dataclass, field from typing import List from .base_activity import ActivityObject from .image import Image + @dataclass(init=False) class Book(ActivityObject): - ''' serializes an edition or work, abstract ''' + """ serializes an edition or work, abstract """ + title: str - sortTitle: str = '' - subtitle: str = '' - description: str = '' + sortTitle: str = "" + subtitle: str = "" + description: str = "" languages: List[str] = field(default_factory=lambda: []) - series: str = '' - seriesNumber: str = '' + series: str = "" + seriesNumber: str = "" subjects: List[str] = field(default_factory=lambda: []) subjectPlaces: List[str] = field(default_factory=lambda: []) authors: List[str] = field(default_factory=lambda: []) - firstPublishedDate: str = '' - publishedDate: str = '' + firstPublishedDate: str = "" + publishedDate: str = "" - openlibraryKey: str = '' - librarythingKey: str = '' - goodreadsKey: str = '' + openlibraryKey: str = "" + librarythingKey: str = "" + goodreadsKey: str = "" cover: Image = None - type: str = 'Book' + type: str = "Book" @dataclass(init=False) class Edition(Book): - ''' Edition instance of a book object ''' + """ Edition instance of a book object """ + work: str - isbn10: str = '' - isbn13: str = '' - oclcNumber: str = '' - asin: str = '' + isbn10: str = "" + isbn13: str = "" + oclcNumber: str = "" + asin: str = "" pages: int = None - physicalFormat: str = '' + physicalFormat: str = "" publishers: List[str] = field(default_factory=lambda: []) editionRank: int = 0 - type: str = 'Edition' + type: str = "Edition" @dataclass(init=False) class Work(Book): - ''' work instance of a book object ''' - lccn: str = '' - defaultEdition: str = '' + """ work instance of a book object """ + + lccn: str = "" + defaultEdition: str = "" editions: List[str] = field(default_factory=lambda: []) - type: str = 'Work' + type: str = "Work" @dataclass(init=False) class Author(ActivityObject): - ''' author of a book ''' + """ author of a book """ + name: str born: str = None died: str = None aliases: List[str] = field(default_factory=lambda: []) - bio: str = '' - openlibraryKey: str = '' - librarythingKey: str = '' - goodreadsKey: str = '' - wikipediaLink: str = '' - type: str = 'Author' + bio: str = "" + openlibraryKey: str = "" + librarythingKey: str = "" + goodreadsKey: str = "" + wikipediaLink: str = "" + type: str = "Author" diff --git a/bookwyrm/activitypub/image.py b/bookwyrm/activitypub/image.py index 569f83c5d..248e7a4ad 100644 --- a/bookwyrm/activitypub/image.py +++ b/bookwyrm/activitypub/image.py @@ -1,11 +1,13 @@ -''' an image, nothing fancy ''' +""" an image, nothing fancy """ from dataclasses import dataclass from .base_activity import ActivityObject + @dataclass(init=False) class Image(ActivityObject): - ''' image block ''' + """ image block """ + url: str - name: str = '' - type: str = 'Image' - id: str = '' + name: str = "" + type: str = "Image" + id: str = "" diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 705d6eede..f38fea5b0 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -1,4 +1,4 @@ -''' note serializer and children thereof ''' +""" note serializer and children thereof """ from dataclasses import dataclass, field from typing import Dict, List from django.apps import apps @@ -6,64 +6,72 @@ from django.apps import apps from .base_activity import ActivityObject, Link from .image import Image + @dataclass(init=False) class Tombstone(ActivityObject): - ''' the placeholder for a deleted status ''' - type: str = 'Tombstone' + """ the placeholder for a deleted status """ + + type: str = "Tombstone" def to_model(self, *args, **kwargs): - ''' this should never really get serialized, just searched for ''' - model = apps.get_model('bookwyrm.Status') + """ this should never really get serialized, just searched for """ + model = apps.get_model("bookwyrm.Status") return model.find_existing_by_remote_id(self.id) @dataclass(init=False) class Note(ActivityObject): - ''' Note activity ''' + """ Note activity """ + published: str attributedTo: str - content: str = '' + content: str = "" to: List[str] = field(default_factory=lambda: []) cc: List[str] = field(default_factory=lambda: []) replies: Dict = field(default_factory=lambda: {}) - inReplyTo: str = '' - summary: str = '' + inReplyTo: str = "" + summary: str = "" tag: List[Link] = field(default_factory=lambda: []) attachment: List[Image] = field(default_factory=lambda: []) sensitive: bool = False - type: str = 'Note' + type: str = "Note" @dataclass(init=False) class Article(Note): - ''' what's an article except a note with more fields ''' + """ what's an article except a note with more fields """ + name: str - type: str = 'Article' + type: str = "Article" @dataclass(init=False) class GeneratedNote(Note): - ''' just a re-typed note ''' - type: str = 'GeneratedNote' + """ just a re-typed note """ + + type: str = "GeneratedNote" @dataclass(init=False) class Comment(Note): - ''' like a note but with a book ''' + """ like a note but with a book """ + inReplyToBook: str - type: str = 'Comment' + type: str = "Comment" @dataclass(init=False) class Review(Comment): - ''' a full book review ''' + """ a full book review """ + name: str = None rating: int = None - type: str = 'Review' + type: str = "Review" @dataclass(init=False) class Quotation(Comment): - ''' a quote and commentary on a book ''' + """ a quote and commentary on a book """ + quote: str - type: str = 'Quotation' + type: str = "Quotation" diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index 14b35f3cf..6da608322 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -1,4 +1,4 @@ -''' defines activitypub collections (lists) ''' +""" defines activitypub collections (lists) """ from dataclasses import dataclass, field from typing import List @@ -7,38 +7,46 @@ from .base_activity import ActivityObject @dataclass(init=False) class OrderedCollection(ActivityObject): - ''' structure of an ordered collection activity ''' + """ structure of an ordered collection activity """ + totalItems: int first: str last: str = None name: str = None owner: str = None - type: str = 'OrderedCollection' + type: str = "OrderedCollection" + @dataclass(init=False) class OrderedCollectionPrivate(OrderedCollection): - ''' an ordered collection with privacy settings ''' + """ an ordered collection with privacy settings """ + to: List[str] = field(default_factory=lambda: []) cc: List[str] = field(default_factory=lambda: []) + @dataclass(init=False) class Shelf(OrderedCollectionPrivate): - ''' structure of an ordered collection activity ''' - type: str = 'Shelf' + """ structure of an ordered collection activity """ + + type: str = "Shelf" + @dataclass(init=False) class BookList(OrderedCollectionPrivate): - ''' structure of an ordered collection activity ''' + """ structure of an ordered collection activity """ + summary: str = None - curation: str = 'closed' - type: str = 'BookList' + curation: str = "closed" + type: str = "BookList" @dataclass(init=False) class OrderedCollectionPage(ActivityObject): - ''' structure of an ordered collection activity ''' + """ structure of an ordered collection activity """ + partOf: str orderedItems: List next: str = None prev: str = None - type: str = 'OrderedCollectionPage' + type: str = "OrderedCollectionPage" diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index 7e7d027eb..ba86b036c 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -1,4 +1,4 @@ -''' actor serializer ''' +""" actor serializer """ from dataclasses import dataclass, field from typing import Dict @@ -8,15 +8,17 @@ from .image import Image @dataclass(init=False) class PublicKey(ActivityObject): - ''' public key block ''' + """ public key block """ + owner: str publicKeyPem: str - type: str = 'PublicKey' + type: str = "PublicKey" @dataclass(init=False) class Person(ActivityObject): - ''' actor activitypub json ''' + """ actor activitypub json """ + preferredUsername: str inbox: str outbox: str @@ -29,4 +31,4 @@ class Person(ActivityObject): bookwyrmUser: bool = False manuallyApprovesFollowers: str = False discoverable: str = True - type: str = 'Person' + type: str = "Person" diff --git a/bookwyrm/activitypub/response.py b/bookwyrm/activitypub/response.py index 8f3c050bb..07f39c7e1 100644 --- a/bookwyrm/activitypub/response.py +++ b/bookwyrm/activitypub/response.py @@ -2,6 +2,7 @@ from django.http import JsonResponse from .base_activity import ActivityEncoder + class ActivitypubResponse(JsonResponse): """ A class to be used in any place that's serializing responses for @@ -9,10 +10,17 @@ class ActivitypubResponse(JsonResponse): configures some stuff beforehand. Made to be a drop-in replacement of JsonResponse. """ - def __init__(self, data, encoder=ActivityEncoder, safe=False, - json_dumps_params=None, **kwargs): - if 'content_type' not in kwargs: - kwargs['content_type'] = 'application/activity+json' + def __init__( + self, + data, + encoder=ActivityEncoder, + safe=False, + json_dumps_params=None, + **kwargs + ): + + if "content_type" not in kwargs: + kwargs["content_type"] = "application/activity+json" super().__init__(data, encoder, safe, json_dumps_params, **kwargs) diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 1236338b2..cd7a757be 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -1,4 +1,4 @@ -''' undo wrapper activity ''' +""" undo wrapper activity """ from dataclasses import dataclass from typing import List from django.apps import apps @@ -9,160 +9,173 @@ from .book import Edition @dataclass(init=False) class Verb(ActivityObject): - ''' generic fields for activities - maybe an unecessary level of - abstraction but w/e ''' + """generic fields for activities - maybe an unecessary level of + abstraction but w/e""" + actor: str object: ActivityObject def action(self): - ''' usually we just want to save, this can be overridden as needed ''' + """ usually we just want to save, this can be overridden as needed """ self.object.to_model() @dataclass(init=False) class Create(Verb): - ''' Create activity ''' + """ Create activity """ + to: List cc: List signature: Signature = None - type: str = 'Create' + type: str = "Create" @dataclass(init=False) class Delete(Verb): - ''' Create activity ''' + """ Create activity """ + to: List cc: List - type: str = 'Delete' + type: str = "Delete" def action(self): - ''' find and delete the activity object ''' + """ find and delete the activity object """ obj = self.object.to_model(save=False, allow_create=False) obj.delete() - @dataclass(init=False) class Update(Verb): - ''' Update activity ''' + """ Update activity """ + to: List - type: str = 'Update' + type: str = "Update" def action(self): - ''' update a model instance from the dataclass ''' + """ update a model instance from the dataclass """ self.object.to_model(allow_create=False) @dataclass(init=False) class Undo(Verb): - ''' Undo an activity ''' - type: str = 'Undo' + """ Undo an activity """ + + type: str = "Undo" def action(self): - ''' find and remove the activity object ''' + """ find and remove the activity object """ # this is so hacky but it does make it work.... # (because you Reject a request and Undo a follow model = None - if self.object.type == 'Follow': - model = apps.get_model('bookwyrm.UserFollows') + if self.object.type == "Follow": + model = apps.get_model("bookwyrm.UserFollows") obj = self.object.to_model(model=model, save=False, allow_create=False) obj.delete() @dataclass(init=False) class Follow(Verb): - ''' Follow activity ''' + """ Follow activity """ + object: str - type: str = 'Follow' + type: str = "Follow" def action(self): - ''' relationship save ''' + """ relationship save """ self.to_model() @dataclass(init=False) class Block(Verb): - ''' Block activity ''' + """ Block activity """ + object: str - type: str = 'Block' + type: str = "Block" def action(self): - ''' relationship save ''' + """ relationship save """ self.to_model() @dataclass(init=False) class Accept(Verb): - ''' Accept activity ''' + """ Accept activity """ + object: Follow - type: str = 'Accept' + type: str = "Accept" def action(self): - ''' find and remove the activity object ''' + """ find and remove the activity object """ obj = self.object.to_model(save=False, allow_create=False) obj.accept() @dataclass(init=False) class Reject(Verb): - ''' Reject activity ''' + """ Reject activity """ + object: Follow - type: str = 'Reject' + type: str = "Reject" def action(self): - ''' find and remove the activity object ''' + """ find and remove the activity object """ obj = self.object.to_model(save=False, allow_create=False) obj.reject() @dataclass(init=False) class Add(Verb): - '''Add activity ''' + """Add activity """ + target: str object: Edition - type: str = 'Add' + type: str = "Add" notes: str = None order: int = 0 approved: bool = True def action(self): - ''' add obj to collection ''' + """ add obj to collection """ target = resolve_remote_id(self.target, refresh=False) # we want to related field that isn't the book, this is janky af sorry - model = [t for t in type(target)._meta.related_objects \ - if t.name != 'edition'][0].related_model + model = [t for t in type(target)._meta.related_objects if t.name != "edition"][ + 0 + ].related_model self.to_model(model=model) @dataclass(init=False) class Remove(Verb): - '''Remove activity ''' + """Remove activity """ + target: ActivityObject - type: str = 'Remove' + type: str = "Remove" def action(self): - ''' find and remove the activity object ''' + """ find and remove the activity object """ obj = self.object.to_model(save=False, allow_create=False) obj.delete() @dataclass(init=False) class Like(Verb): - ''' a user faving an object ''' + """ a user faving an object """ + object: str - type: str = 'Like' + type: str = "Like" def action(self): - ''' like ''' + """ like """ self.to_model() @dataclass(init=False) class Announce(Verb): - ''' boosting a status ''' + """ boosting a status """ + object: str - type: str = 'Announce' + type: str = "Announce" def action(self): - ''' boost ''' + """ boost """ self.to_model() diff --git a/bookwyrm/admin.py b/bookwyrm/admin.py index 45af81d99..efe5e9d72 100644 --- a/bookwyrm/admin.py +++ b/bookwyrm/admin.py @@ -1,4 +1,4 @@ -''' models that will show up in django admin for superuser ''' +""" models that will show up in django admin for superuser """ from django.contrib import admin from bookwyrm import models diff --git a/bookwyrm/connectors/__init__.py b/bookwyrm/connectors/__init__.py index cfafd2868..689f27018 100644 --- a/bookwyrm/connectors/__init__.py +++ b/bookwyrm/connectors/__init__.py @@ -1,4 +1,4 @@ -''' bring connectors into the namespace ''' +""" bring connectors into the namespace """ from .settings import CONNECTORS from .abstract_connector import ConnectorException from .abstract_connector import get_data, get_image diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index e6372438e..9f31b337d 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -1,4 +1,4 @@ -''' functionality outline for a book data connector ''' +""" functionality outline for a book data connector """ from abc import ABC, abstractmethod from dataclasses import asdict, dataclass import logging @@ -13,8 +13,11 @@ from .connector_manager import load_more_data, ConnectorException logger = logging.getLogger(__name__) + + class AbstractMinimalConnector(ABC): - ''' just the bare bones, for other bookwyrm instances ''' + """ just the bare bones, for other bookwyrm instances """ + def __init__(self, identifier): # load connector settings info = models.Connector.objects.get(identifier=identifier) @@ -22,31 +25,31 @@ class AbstractMinimalConnector(ABC): # the things in the connector model to copy over self_fields = [ - 'base_url', - 'books_url', - 'covers_url', - 'search_url', - 'isbn_search_url', - 'max_query_count', - 'name', - 'identifier', - 'local' + "base_url", + "books_url", + "covers_url", + "search_url", + "isbn_search_url", + "max_query_count", + "name", + "identifier", + "local", ] for field in self_fields: setattr(self, field, getattr(info, field)) def search(self, query, min_confidence=None): - ''' free text search ''' + """ free text search """ params = {} if min_confidence: - params['min_confidence'] = min_confidence + params["min_confidence"] = min_confidence resp = requests.get( - '%s%s' % (self.search_url, query), + "%s%s" % (self.search_url, query), params=params, headers={ - 'Accept': 'application/json; charset=utf-8', - 'User-Agent': settings.USER_AGENT, + "Accept": "application/json; charset=utf-8", + "User-Agent": settings.USER_AGENT, }, ) if not resp.ok: @@ -55,7 +58,7 @@ class AbstractMinimalConnector(ABC): data = resp.json() except ValueError as e: logger.exception(e) - raise ConnectorException('Unable to parse json response', e) + raise ConnectorException("Unable to parse json response", e) results = [] for doc in self.parse_search_data(data)[:10]: @@ -63,14 +66,14 @@ class AbstractMinimalConnector(ABC): return results def isbn_search(self, query): - ''' isbn search ''' + """ isbn search """ params = {} resp = requests.get( - '%s%s' % (self.isbn_search_url, query), + "%s%s" % (self.isbn_search_url, query), params=params, headers={ - 'Accept': 'application/json; charset=utf-8', - 'User-Agent': settings.USER_AGENT, + "Accept": "application/json; charset=utf-8", + "User-Agent": settings.USER_AGENT, }, ) if not resp.ok: @@ -79,7 +82,7 @@ class AbstractMinimalConnector(ABC): data = resp.json() except ValueError as e: logger.exception(e) - raise ConnectorException('Unable to parse json response', e) + raise ConnectorException("Unable to parse json response", e) results = [] for doc in self.parse_isbn_search_data(data): @@ -88,49 +91,49 @@ class AbstractMinimalConnector(ABC): @abstractmethod def get_or_create_book(self, remote_id): - ''' pull up a book record by whatever means possible ''' + """ pull up a book record by whatever means possible """ @abstractmethod def parse_search_data(self, data): - ''' turn the result json from a search into a list ''' + """ turn the result json from a search into a list """ @abstractmethod def format_search_result(self, search_result): - ''' create a SearchResult obj from json ''' + """ create a SearchResult obj from json """ @abstractmethod def parse_isbn_search_data(self, data): - ''' turn the result json from a search into a list ''' + """ turn the result json from a search into a list """ @abstractmethod def format_isbn_search_result(self, search_result): - ''' create a SearchResult obj from json ''' + """ create a SearchResult obj from json """ class AbstractConnector(AbstractMinimalConnector): - ''' generic book data connector ''' + """ generic book data connector """ + def __init__(self, identifier): super().__init__(identifier) # fields we want to look for in book data to copy over # title we handle separately. self.book_mappings = [] - def is_available(self): - ''' check if you're allowed to use this connector ''' + """ check if you're allowed to use this connector """ if self.max_query_count is not None: if self.connector.query_count >= self.max_query_count: return False return True - def get_or_create_book(self, remote_id): - ''' translate arbitrary json into an Activitypub dataclass ''' + """ translate arbitrary json into an Activitypub dataclass """ # first, check if we have the origin_id saved - existing = models.Edition.find_existing_by_remote_id(remote_id) or \ - models.Work.find_existing_by_remote_id(remote_id) + existing = models.Edition.find_existing_by_remote_id( + remote_id + ) or models.Work.find_existing_by_remote_id(remote_id) if existing: - if hasattr(existing, 'get_default_editon'): + if hasattr(existing, "get_default_editon"): return existing.get_default_editon() return existing @@ -154,7 +157,7 @@ class AbstractConnector(AbstractMinimalConnector): edition_data = data if not work_data or not edition_data: - raise ConnectorException('Unable to load book data: %s' % remote_id) + raise ConnectorException("Unable to load book data: %s" % remote_id) with transaction.atomic(): # create activitypub object @@ -168,11 +171,10 @@ class AbstractConnector(AbstractMinimalConnector): load_more_data.delay(self.connector.id, work.id) return edition - def create_edition_from_data(self, work, edition_data): - ''' if we already have the work, we're ready ''' + """ if we already have the work, we're ready """ mapped_data = dict_from_mappings(edition_data, self.book_mappings) - mapped_data['work'] = work.remote_id + mapped_data["work"] = work.remote_id edition_activity = activitypub.Edition(**mapped_data) edition = edition_activity.to_model(model=models.Edition) edition.connector = self.connector @@ -189,9 +191,8 @@ class AbstractConnector(AbstractMinimalConnector): return edition - def get_or_create_author(self, remote_id): - ''' load that author ''' + """ load that author """ existing = models.Author.find_existing_by_remote_id(remote_id) if existing: return existing @@ -203,31 +204,30 @@ class AbstractConnector(AbstractMinimalConnector): # this will dedupe return activity.to_model(model=models.Author) - @abstractmethod def is_work_data(self, data): - ''' differentiate works and editions ''' + """ differentiate works and editions """ @abstractmethod def get_edition_from_work_data(self, data): - ''' every work needs at least one edition ''' + """ every work needs at least one edition """ @abstractmethod def get_work_from_edition_data(self, data): - ''' every edition needs a work ''' + """ every edition needs a work """ @abstractmethod def get_authors_from_data(self, data): - ''' load author data ''' + """ load author data """ @abstractmethod def expand_book_data(self, book): - ''' get more info on a book ''' + """ get more info on a book """ def dict_from_mappings(data, mappings): - ''' create a dict in Activitypub format, using mappings supplies by - the subclass ''' + """create a dict in Activitypub format, using mappings supplies by + the subclass""" result = {} for mapping in mappings: result[mapping.local_field] = mapping.get_value(data) @@ -235,13 +235,13 @@ def dict_from_mappings(data, mappings): def get_data(url): - ''' wrapper for request.get ''' + """ wrapper for request.get """ try: resp = requests.get( url, headers={ - 'Accept': 'application/json; charset=utf-8', - 'User-Agent': settings.USER_AGENT, + "Accept": "application/json; charset=utf-8", + "User-Agent": settings.USER_AGENT, }, ) except (RequestError, SSLError) as e: @@ -260,12 +260,12 @@ def get_data(url): def get_image(url): - ''' wrapper for requesting an image ''' + """ wrapper for requesting an image """ try: resp = requests.get( url, headers={ - 'User-Agent': settings.USER_AGENT, + "User-Agent": settings.USER_AGENT, }, ) except (RequestError, SSLError) as e: @@ -278,7 +278,8 @@ def get_image(url): @dataclass class SearchResult: - ''' standardized search result object ''' + """ standardized search result object """ + title: str key: str author: str @@ -288,17 +289,19 @@ class SearchResult: def __repr__(self): return "".format( - self.key, self.title, self.author) + self.key, self.title, self.author + ) def json(self): - ''' serialize a connector for json response ''' + """ serialize a connector for json response """ serialized = asdict(self) - del serialized['connector'] + del serialized["connector"] return serialized class Mapping: - ''' associate a local database field with a field in an external dataset ''' + """ associate a local database field with a field in an external dataset """ + def __init__(self, local_field, remote_field=None, formatter=None): noop = lambda x: x @@ -307,11 +310,11 @@ class Mapping: self.formatter = formatter or noop def get_value(self, data): - ''' pull a field from incoming json and return the formatted version ''' + """ pull a field from incoming json and return the formatted version """ value = data.get(self.remote_field) if not value: return None try: return self.formatter(value) - except:# pylint: disable=bare-except + except: # pylint: disable=bare-except return None diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index 96b72f267..742d7e858 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -1,10 +1,10 @@ -''' using another bookwyrm instance as a source of book data ''' +""" using another bookwyrm instance as a source of book data """ from bookwyrm import activitypub, models from .abstract_connector import AbstractMinimalConnector, SearchResult class Connector(AbstractMinimalConnector): - ''' this is basically just for search ''' + """ this is basically just for search """ def get_or_create_book(self, remote_id): edition = activitypub.resolve_remote_id(remote_id, model=models.Edition) @@ -17,13 +17,12 @@ class Connector(AbstractMinimalConnector): return data def format_search_result(self, search_result): - search_result['connector'] = self + search_result["connector"] = self return SearchResult(**search_result) def parse_isbn_search_data(self, data): return data def format_isbn_search_result(self, search_result): - search_result['connector'] = self + search_result["connector"] = self return SearchResult(**search_result) - diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index 053e1f9ef..3891d02a4 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -1,4 +1,4 @@ -''' interface with whatever connectors the app has ''' +""" interface with whatever connectors the app has """ import importlib import re from urllib.parse import urlparse @@ -10,24 +10,24 @@ from bookwyrm.tasks import app class ConnectorException(HTTPError): - ''' when the connector can't do what was asked ''' + """ when the connector can't do what was asked """ def search(query, min_confidence=0.1): - ''' find books based on arbitary keywords ''' + """ find books based on arbitary keywords """ results = [] # Have we got a ISBN ? - isbn = re.sub('[\W_]', '', query) - maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 + isbn = re.sub("[\W_]", "", query) + maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 - dedup_slug = lambda r: '%s/%s/%s' % (r.title, r.author, r.year) + dedup_slug = lambda r: "%s/%s/%s" % (r.title, r.author, r.year) result_index = set() for connector in get_connectors(): result_set = None if maybe_isbn: # Search on ISBN - if not connector.isbn_search_url or connector.isbn_search_url == '': + if not connector.isbn_search_url or connector.isbn_search_url == "": result_set = [] else: try: @@ -42,32 +42,33 @@ def search(query, min_confidence=0.1): except (HTTPError, ConnectorException): continue - result_set = [r for r in result_set \ - if dedup_slug(r) not in result_index] + result_set = [r for r in result_set if dedup_slug(r) not in result_index] # `|=` concats two sets. WE ARE GETTING FANCY HERE result_index |= set(dedup_slug(r) for r in result_set) - results.append({ - 'connector': connector, - 'results': result_set, - }) + results.append( + { + "connector": connector, + "results": result_set, + } + ) return results def local_search(query, min_confidence=0.1, raw=False): - ''' only look at local search results ''' + """ only look at local search results """ connector = load_connector(models.Connector.objects.get(local=True)) return connector.search(query, min_confidence=min_confidence, raw=raw) def isbn_local_search(query, raw=False): - ''' only look at local search results ''' + """ only look at local search results """ connector = load_connector(models.Connector.objects.get(local=True)) return connector.isbn_search(query, raw=raw) def first_search_result(query, min_confidence=0.1): - ''' search until you find a result that fits ''' + """ search until you find a result that fits """ for connector in get_connectors(): result = connector.search(query, min_confidence=min_confidence) if result: @@ -76,29 +77,29 @@ def first_search_result(query, min_confidence=0.1): def get_connectors(): - ''' load all connectors ''' - for info in models.Connector.objects.order_by('priority').all(): + """ load all connectors """ + for info in models.Connector.objects.order_by("priority").all(): yield load_connector(info) def get_or_create_connector(remote_id): - ''' get the connector related to the author's server ''' + """ get the connector related to the author's server """ url = urlparse(remote_id) identifier = url.netloc if not identifier: - raise ValueError('Invalid remote id') + raise ValueError("Invalid remote id") try: connector_info = models.Connector.objects.get(identifier=identifier) except models.Connector.DoesNotExist: connector_info = models.Connector.objects.create( identifier=identifier, - connector_file='bookwyrm_connector', - base_url='https://%s' % identifier, - books_url='https://%s/book' % identifier, - covers_url='https://%s/images/covers' % identifier, - search_url='https://%s/search?q=' % identifier, - priority=2 + connector_file="bookwyrm_connector", + base_url="https://%s" % identifier, + books_url="https://%s/book" % identifier, + covers_url="https://%s/images/covers" % identifier, + search_url="https://%s/search?q=" % identifier, + priority=2, ) return load_connector(connector_info) @@ -106,7 +107,7 @@ def get_or_create_connector(remote_id): @app.task def load_more_data(connector_id, book_id): - ''' background the work of getting all 10,000 editions of LoTR ''' + """ background the work of getting all 10,000 editions of LoTR """ connector_info = models.Connector.objects.get(id=connector_id) connector = load_connector(connector_info) book = models.Book.objects.select_subclasses().get(id=book_id) @@ -114,8 +115,8 @@ def load_more_data(connector_id, book_id): def load_connector(connector_info): - ''' instantiate the connector class ''' + """ instantiate the connector class """ connector = importlib.import_module( - 'bookwyrm.connectors.%s' % connector_info.connector_file + "bookwyrm.connectors.%s" % connector_info.connector_file ) return connector.Connector(connector_info.identifier) diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index 8d227eef1..fb9a4e477 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -1,4 +1,4 @@ -''' openlibrary data connector ''' +""" openlibrary data connector """ import re from bookwyrm import models @@ -9,148 +9,134 @@ from .openlibrary_languages import languages class Connector(AbstractConnector): - ''' instantiate a connector for OL ''' + """ instantiate a connector for OL """ + def __init__(self, identifier): super().__init__(identifier) get_first = lambda a: a[0] get_remote_id = lambda a: self.base_url + a self.book_mappings = [ - Mapping('title'), - Mapping('id', remote_field='key', formatter=get_remote_id), + Mapping("title"), + Mapping("id", remote_field="key", formatter=get_remote_id), + Mapping("cover", remote_field="covers", formatter=self.get_cover_url), + Mapping("sortTitle", remote_field="sort_title"), + Mapping("subtitle"), + Mapping("description", formatter=get_description), + Mapping("languages", formatter=get_languages), + Mapping("series", formatter=get_first), + Mapping("seriesNumber", remote_field="series_number"), + Mapping("subjects"), + Mapping("subjectPlaces", remote_field="subject_places"), + Mapping("isbn13", remote_field="isbn_13", formatter=get_first), + Mapping("isbn10", remote_field="isbn_10", formatter=get_first), + Mapping("lccn", formatter=get_first), + Mapping("oclcNumber", remote_field="oclc_numbers", formatter=get_first), Mapping( - 'cover', remote_field='covers', formatter=self.get_cover_url), - Mapping('sortTitle', remote_field='sort_title'), - Mapping('subtitle'), - Mapping('description', formatter=get_description), - Mapping('languages', formatter=get_languages), - Mapping('series', formatter=get_first), - Mapping('seriesNumber', remote_field='series_number'), - Mapping('subjects'), - Mapping('subjectPlaces', remote_field='subject_places'), - Mapping('isbn13', remote_field='isbn_13', formatter=get_first), - Mapping('isbn10', remote_field='isbn_10', formatter=get_first), - Mapping('lccn', formatter=get_first), - Mapping( - 'oclcNumber', remote_field='oclc_numbers', - formatter=get_first + "openlibraryKey", remote_field="key", formatter=get_openlibrary_key ), + Mapping("goodreadsKey", remote_field="goodreads_key"), + Mapping("asin"), Mapping( - 'openlibraryKey', remote_field='key', - formatter=get_openlibrary_key + "firstPublishedDate", + remote_field="first_publish_date", ), - Mapping('goodreadsKey', remote_field='goodreads_key'), - Mapping('asin'), - Mapping( - 'firstPublishedDate', remote_field='first_publish_date', - ), - Mapping('publishedDate', remote_field='publish_date'), - Mapping('pages', remote_field='number_of_pages'), - Mapping('physicalFormat', remote_field='physical_format'), - Mapping('publishers'), + Mapping("publishedDate", remote_field="publish_date"), + Mapping("pages", remote_field="number_of_pages"), + Mapping("physicalFormat", remote_field="physical_format"), + Mapping("publishers"), ] self.author_mappings = [ - Mapping('id', remote_field='key', formatter=get_remote_id), - Mapping('name'), + Mapping("id", remote_field="key", formatter=get_remote_id), + Mapping("name"), Mapping( - 'openlibraryKey', remote_field='key', - formatter=get_openlibrary_key + "openlibraryKey", remote_field="key", formatter=get_openlibrary_key ), - Mapping('born', remote_field='birth_date'), - Mapping('died', remote_field='death_date'), - Mapping('bio', formatter=get_description), + Mapping("born", remote_field="birth_date"), + Mapping("died", remote_field="death_date"), + Mapping("bio", formatter=get_description), ] - def get_remote_id_from_data(self, data): - ''' format a url from an openlibrary id field ''' + """ format a url from an openlibrary id field """ try: - key = data['key'] + key = data["key"] except KeyError: - raise ConnectorException('Invalid book data') - return '%s%s' % (self.books_url, key) - + raise ConnectorException("Invalid book data") + return "%s%s" % (self.books_url, key) def is_work_data(self, data): - return bool(re.match(r'^[\/\w]+OL\d+W$', data['key'])) - + return bool(re.match(r"^[\/\w]+OL\d+W$", data["key"])) def get_edition_from_work_data(self, data): try: - key = data['key'] + key = data["key"] except KeyError: - raise ConnectorException('Invalid book data') - url = '%s%s/editions' % (self.books_url, key) + raise ConnectorException("Invalid book data") + url = "%s%s/editions" % (self.books_url, key) data = get_data(url) - return pick_default_edition(data['entries']) - + return pick_default_edition(data["entries"]) def get_work_from_edition_data(self, data): try: - key = data['works'][0]['key'] + key = data["works"][0]["key"] except (IndexError, KeyError): - raise ConnectorException('No work found for edition') - url = '%s%s' % (self.books_url, key) + raise ConnectorException("No work found for edition") + url = "%s%s" % (self.books_url, key) return get_data(url) - def get_authors_from_data(self, data): - ''' parse author json and load or create authors ''' - for author_blob in data.get('authors', []): - author_blob = author_blob.get('author', author_blob) + """ parse author json and load or create authors """ + for author_blob in data.get("authors", []): + author_blob = author_blob.get("author", author_blob) # this id is "/authors/OL1234567A" - author_id = author_blob['key'] - url = '%s%s' % (self.base_url, author_id) + author_id = author_blob["key"] + url = "%s%s" % (self.base_url, author_id) yield self.get_or_create_author(url) - def get_cover_url(self, cover_blob): - ''' ask openlibrary for the cover ''' + """ ask openlibrary for the cover """ cover_id = cover_blob[0] - image_name = '%s-L.jpg' % cover_id - return '%s/b/id/%s' % (self.covers_url, image_name) - + image_name = "%s-L.jpg" % cover_id + return "%s/b/id/%s" % (self.covers_url, image_name) def parse_search_data(self, data): - return data.get('docs') - + return data.get("docs") def format_search_result(self, search_result): # build the remote id from the openlibrary key - key = self.books_url + search_result['key'] - author = search_result.get('author_name') or ['Unknown'] + key = self.books_url + search_result["key"] + author = search_result.get("author_name") or ["Unknown"] return SearchResult( - title=search_result.get('title'), + title=search_result.get("title"), key=key, - author=', '.join(author), + author=", ".join(author), connector=self, - year=search_result.get('first_publish_year'), + year=search_result.get("first_publish_year"), ) - def parse_isbn_search_data(self, data): return list(data.values()) def format_isbn_search_result(self, search_result): # build the remote id from the openlibrary key - key = self.books_url + search_result['key'] - authors = search_result.get('authors') or [{'name': 'Unknown'}] - author_names = [ author.get('name') for author in authors] + key = self.books_url + search_result["key"] + authors = search_result.get("authors") or [{"name": "Unknown"}] + author_names = [author.get("name") for author in authors] return SearchResult( - title=search_result.get('title'), + title=search_result.get("title"), key=key, - author=', '.join(author_names), + author=", ".join(author_names), connector=self, - year=search_result.get('publish_date'), + year=search_result.get("publish_date"), ) def load_edition_data(self, olkey): - ''' query openlibrary for editions of a work ''' - url = '%s/works/%s/editions' % (self.books_url, olkey) + """ query openlibrary for editions of a work """ + url = "%s/works/%s/editions" % (self.books_url, olkey) return get_data(url) - def expand_book_data(self, book): work = book # go from the edition to the work, if necessary @@ -164,7 +150,7 @@ class Connector(AbstractConnector): # who knows, man return - for edition_data in edition_options.get('entries'): + for edition_data in edition_options.get("entries"): # does this edition have ANY interesting data? if ignore_edition(edition_data): continue @@ -172,62 +158,63 @@ class Connector(AbstractConnector): def ignore_edition(edition_data): - ''' don't load a million editions that have no metadata ''' + """ don't load a million editions that have no metadata """ # an isbn, we love to see it - if edition_data.get('isbn_13') or edition_data.get('isbn_10'): - print(edition_data.get('isbn_10')) + if edition_data.get("isbn_13") or edition_data.get("isbn_10"): + print(edition_data.get("isbn_10")) return False # grudgingly, oclc can stay - if edition_data.get('oclc_numbers'): - print(edition_data.get('oclc_numbers')) + if edition_data.get("oclc_numbers"): + print(edition_data.get("oclc_numbers")) return False # if it has a cover it can stay - if edition_data.get('covers'): - print(edition_data.get('covers')) + if edition_data.get("covers"): + print(edition_data.get("covers")) return False # keep non-english editions - if edition_data.get('languages') and \ - 'languages/eng' not in str(edition_data.get('languages')): - print(edition_data.get('languages')) + if edition_data.get("languages") and "languages/eng" not in str( + edition_data.get("languages") + ): + print(edition_data.get("languages")) return False return True def get_description(description_blob): - ''' descriptions can be a string or a dict ''' + """ descriptions can be a string or a dict """ if isinstance(description_blob, dict): - return description_blob.get('value') + return description_blob.get("value") return description_blob def get_openlibrary_key(key): - ''' convert /books/OL27320736M into OL27320736M ''' - return key.split('/')[-1] + """ convert /books/OL27320736M into OL27320736M """ + return key.split("/")[-1] def get_languages(language_blob): - ''' /language/eng -> English ''' + """ /language/eng -> English """ langs = [] for lang in language_blob: - langs.append( - languages.get(lang.get('key', ''), None) - ) + langs.append(languages.get(lang.get("key", ""), None)) return langs def pick_default_edition(options): - ''' favor physical copies with covers in english ''' + """ favor physical copies with covers in english """ if not options: return None if len(options) == 1: return options[0] - options = [e for e in options if e.get('covers')] or options - options = [e for e in options if \ - '/languages/eng' in str(e.get('languages'))] or options - formats = ['paperback', 'hardcover', 'mass market paperback'] - options = [e for e in options if \ - str(e.get('physical_format')).lower() in formats] or options - options = [e for e in options if e.get('isbn_13')] or options - options = [e for e in options if e.get('ocaid')] or options + options = [e for e in options if e.get("covers")] or options + options = [ + e for e in options if "/languages/eng" in str(e.get("languages")) + ] or options + formats = ["paperback", "hardcover", "mass market paperback"] + options = [ + e for e in options if str(e.get("physical_format")).lower() in formats + ] or options + options = [e for e in options if e.get("isbn_13")] or options + options = [e for e in options if e.get("ocaid")] or options return options[0] diff --git a/bookwyrm/connectors/openlibrary_languages.py b/bookwyrm/connectors/openlibrary_languages.py index b687f8b97..2520d1ea1 100644 --- a/bookwyrm/connectors/openlibrary_languages.py +++ b/bookwyrm/connectors/openlibrary_languages.py @@ -1,467 +1,467 @@ -''' key lookups for openlibrary languages ''' +""" key lookups for openlibrary languages """ languages = { - '/languages/eng': 'English', - '/languages/fre': 'French', - '/languages/spa': 'Spanish', - '/languages/ger': 'German', - '/languages/rus': 'Russian', - '/languages/ita': 'Italian', - '/languages/chi': 'Chinese', - '/languages/jpn': 'Japanese', - '/languages/por': 'Portuguese', - '/languages/ara': 'Arabic', - '/languages/pol': 'Polish', - '/languages/heb': 'Hebrew', - '/languages/kor': 'Korean', - '/languages/dut': 'Dutch', - '/languages/ind': 'Indonesian', - '/languages/lat': 'Latin', - '/languages/und': 'Undetermined', - '/languages/cmn': 'Mandarin', - '/languages/hin': 'Hindi', - '/languages/swe': 'Swedish', - '/languages/dan': 'Danish', - '/languages/urd': 'Urdu', - '/languages/hun': 'Hungarian', - '/languages/cze': 'Czech', - '/languages/tur': 'Turkish', - '/languages/ukr': 'Ukrainian', - '/languages/gre': 'Greek', - '/languages/vie': 'Vietnamese', - '/languages/bul': 'Bulgarian', - '/languages/ben': 'Bengali', - '/languages/rum': 'Romanian', - '/languages/cat': 'Catalan', - '/languages/nor': 'Norwegian', - '/languages/tha': 'Thai', - '/languages/per': 'Persian', - '/languages/scr': 'Croatian', - '/languages/mul': 'Multiple languages', - '/languages/fin': 'Finnish', - '/languages/tam': 'Tamil', - '/languages/guj': 'Gujarati', - '/languages/mar': 'Marathi', - '/languages/scc': 'Serbian', - '/languages/pan': 'Panjabi', - '/languages/wel': 'Welsh', - '/languages/tel': 'Telugu', - '/languages/yid': 'Yiddish', - '/languages/kan': 'Kannada', - '/languages/slo': 'Slovak', - '/languages/san': 'Sanskrit', - '/languages/arm': 'Armenian', - '/languages/mal': 'Malayalam', - '/languages/may': 'Malay', - '/languages/bur': 'Burmese', - '/languages/slv': 'Slovenian', - '/languages/lit': 'Lithuanian', - '/languages/tib': 'Tibetan', - '/languages/lav': 'Latvian', - '/languages/est': 'Estonian', - '/languages/nep': 'Nepali', - '/languages/ori': 'Oriya', - '/languages/mon': 'Mongolian', - '/languages/alb': 'Albanian', - '/languages/iri': 'Irish', - '/languages/geo': 'Georgian', - '/languages/afr': 'Afrikaans', - '/languages/grc': 'Ancient Greek', - '/languages/mac': 'Macedonian', - '/languages/bel': 'Belarusian', - '/languages/ice': 'Icelandic', - '/languages/srp': 'Serbian', - '/languages/snh': 'Sinhalese', - '/languages/snd': 'Sindhi', - '/languages/ota': 'Turkish, Ottoman', - '/languages/kur': 'Kurdish', - '/languages/aze': 'Azerbaijani', - '/languages/pus': 'Pushto', - '/languages/amh': 'Amharic', - '/languages/gag': 'Galician', - '/languages/hrv': 'Croatian', - '/languages/sin': 'Sinhalese', - '/languages/asm': 'Assamese', - '/languages/uzb': 'Uzbek', - '/languages/gae': 'Scottish Gaelix', - '/languages/kaz': 'Kazakh', - '/languages/swa': 'Swahili', - '/languages/bos': 'Bosnian', - '/languages/glg': 'Galician ', - '/languages/baq': 'Basque', - '/languages/tgl': 'Tagalog', - '/languages/raj': 'Rajasthani', - '/languages/gle': 'Irish', - '/languages/lao': 'Lao', - '/languages/jav': 'Javanese', - '/languages/mai': 'Maithili', - '/languages/tgk': 'Tajik ', - '/languages/khm': 'Khmer', - '/languages/roh': 'Raeto-Romance', - '/languages/kok': 'Konkani ', - '/languages/sit': 'Sino-Tibetan (Other)', - '/languages/mol': 'Moldavian', - '/languages/kir': 'Kyrgyz', - '/languages/new': 'Newari', - '/languages/inc': 'Indic (Other)', - '/languages/frm': 'French, Middle (ca. 1300-1600)', - '/languages/esp': 'Esperanto', - '/languages/hau': 'Hausa', - '/languages/tag': 'Tagalog', - '/languages/tuk': 'Turkmen', - '/languages/enm': 'English, Middle (1100-1500)', - '/languages/map': 'Austronesian (Other)', - '/languages/pli': 'Pali', - '/languages/fro': 'French, Old (ca. 842-1300)', - '/languages/nic': 'Niger-Kordofanian (Other)', - '/languages/tir': 'Tigrinya', - '/languages/wen': 'Sorbian (Other)', - '/languages/bho': 'Bhojpuri', - '/languages/roa': 'Romance (Other)', - '/languages/tut': 'Altaic (Other)', - '/languages/bra': 'Braj', - '/languages/sun': 'Sundanese', - '/languages/fiu': 'Finno-Ugrian (Other)', - '/languages/far': 'Faroese', - '/languages/ban': 'Balinese', - '/languages/tar': 'Tatar', - '/languages/bak': 'Bashkir', - '/languages/tat': 'Tatar', - '/languages/chu': 'Church Slavic', - '/languages/dra': 'Dravidian (Other)', - '/languages/pra': 'Prakrit languages', - '/languages/paa': 'Papuan (Other)', - '/languages/doi': 'Dogri', - '/languages/lah': 'Lahndā', - '/languages/mni': 'Manipuri', - '/languages/yor': 'Yoruba', - '/languages/gmh': 'German, Middle High (ca. 1050-1500)', - '/languages/kas': 'Kashmiri', - '/languages/fri': 'Frisian', - '/languages/mla': 'Malagasy', - '/languages/egy': 'Egyptian', - '/languages/rom': 'Romani', - '/languages/syr': 'Syriac, Modern', - '/languages/cau': 'Caucasian (Other)', - '/languages/hbs': 'Serbo-Croatian', - '/languages/sai': 'South American Indian (Other)', - '/languages/pro': 'Provençal (to 1500)', - '/languages/cpf': 'Creoles and Pidgins, French-based (Other)', - '/languages/ang': 'English, Old (ca. 450-1100)', - '/languages/bal': 'Baluchi', - '/languages/gla': 'Scottish Gaelic', - '/languages/chv': 'Chuvash', - '/languages/kin': 'Kinyarwanda', - '/languages/zul': 'Zulu', - '/languages/sla': 'Slavic (Other)', - '/languages/som': 'Somali', - '/languages/mlt': 'Maltese', - '/languages/uig': 'Uighur', - '/languages/mlg': 'Malagasy', - '/languages/sho': 'Shona', - '/languages/lan': 'Occitan (post 1500)', - '/languages/bre': 'Breton', - '/languages/sco': 'Scots', - '/languages/sso': 'Sotho', - '/languages/myn': 'Mayan languages', - '/languages/xho': 'Xhosa', - '/languages/gem': 'Germanic (Other)', - '/languages/esk': 'Eskimo languages', - '/languages/akk': 'Akkadian', - '/languages/div': 'Maldivian', - '/languages/sah': 'Yakut', - '/languages/tsw': 'Tswana', - '/languages/nso': 'Northern Sotho', - '/languages/pap': 'Papiamento', - '/languages/bnt': 'Bantu (Other)', - '/languages/oss': 'Ossetic', - '/languages/cre': 'Cree', - '/languages/ibo': 'Igbo', - '/languages/fao': 'Faroese', - '/languages/nai': 'North American Indian (Other)', - '/languages/mag': 'Magahi', - '/languages/arc': 'Aramaic', - '/languages/epo': 'Esperanto', - '/languages/kha': 'Khasi', - '/languages/oji': 'Ojibwa', - '/languages/que': 'Quechua', - '/languages/lug': 'Ganda', - '/languages/mwr': 'Marwari', - '/languages/awa': 'Awadhi ', - '/languages/cor': 'Cornish', - '/languages/lad': 'Ladino', - '/languages/dzo': 'Dzongkha', - '/languages/cop': 'Coptic', - '/languages/nah': 'Nahuatl', - '/languages/cai': 'Central American Indian (Other)', - '/languages/phi': 'Philippine (Other)', - '/languages/moh': 'Mohawk', - '/languages/crp': 'Creoles and Pidgins (Other)', - '/languages/nya': 'Nyanja', - '/languages/wol': 'Wolof ', - '/languages/haw': 'Hawaiian', - '/languages/eth': 'Ethiopic', - '/languages/mis': 'Miscellaneous languages', - '/languages/mkh': 'Mon-Khmer (Other)', - '/languages/alg': 'Algonquian (Other)', - '/languages/nde': 'Ndebele (Zimbabwe)', - '/languages/ssa': 'Nilo-Saharan (Other)', - '/languages/chm': 'Mari', - '/languages/che': 'Chechen', - '/languages/gez': 'Ethiopic', - '/languages/ven': 'Venda', - '/languages/cam': 'Khmer', - '/languages/fur': 'Friulian', - '/languages/ful': 'Fula', - '/languages/gal': 'Oromo', - '/languages/jrb': 'Judeo-Arabic', - '/languages/bua': 'Buriat', - '/languages/ady': 'Adygei', - '/languages/bem': 'Bemba', - '/languages/kar': 'Karen languages', - '/languages/sna': 'Shona', - '/languages/twi': 'Twi', - '/languages/btk': 'Batak', - '/languages/kaa': 'Kara-Kalpak', - '/languages/kom': 'Komi', - '/languages/sot': 'Sotho', - '/languages/tso': 'Tsonga', - '/languages/cpe': 'Creoles and Pidgins, English-based (Other)', - '/languages/gua': 'Guarani', - '/languages/mao': 'Maori', - '/languages/mic': 'Micmac', - '/languages/swz': 'Swazi', - '/languages/taj': 'Tajik', - '/languages/smo': 'Samoan', - '/languages/ace': 'Achinese', - '/languages/afa': 'Afroasiatic (Other)', - '/languages/lap': 'Sami', - '/languages/min': 'Minangkabau', - '/languages/oci': 'Occitan (post 1500)', - '/languages/tsn': 'Tswana', - '/languages/pal': 'Pahlavi', - '/languages/sux': 'Sumerian', - '/languages/ewe': 'Ewe', - '/languages/him': 'Himachali', - '/languages/kaw': 'Kawi', - '/languages/lus': 'Lushai', - '/languages/ceb': 'Cebuano', - '/languages/chr': 'Cherokee', - '/languages/fil': 'Filipino', - '/languages/ndo': 'Ndonga', - '/languages/ilo': 'Iloko', - '/languages/kbd': 'Kabardian', - '/languages/orm': 'Oromo', - '/languages/dum': 'Dutch, Middle (ca. 1050-1350)', - '/languages/bam': 'Bambara', - '/languages/goh': 'Old High German', - '/languages/got': 'Gothic', - '/languages/kon': 'Kongo', - '/languages/mun': 'Munda (Other)', - '/languages/kru': 'Kurukh', - '/languages/pam': 'Pampanga', - '/languages/grn': 'Guarani', - '/languages/gaa': 'Gã', - '/languages/fry': 'Frisian', - '/languages/iba': 'Iban', - '/languages/mak': 'Makasar', - '/languages/kik': 'Kikuyu', - '/languages/cho': 'Choctaw', - '/languages/cpp': 'Creoles and Pidgins, Portuguese-based (Other)', - '/languages/dak': 'Dakota', - '/languages/udm': 'Udmurt ', - '/languages/hat': 'Haitian French Creole', - '/languages/mus': 'Creek', - '/languages/ber': 'Berber (Other)', - '/languages/hil': 'Hiligaynon', - '/languages/iro': 'Iroquoian (Other)', - '/languages/kua': 'Kuanyama', - '/languages/mno': 'Manobo languages', - '/languages/run': 'Rundi', - '/languages/sat': 'Santali', - '/languages/shn': 'Shan', - '/languages/tyv': 'Tuvinian', - '/languages/chg': 'Chagatai', - '/languages/syc': 'Syriac', - '/languages/ath': 'Athapascan (Other)', - '/languages/aym': 'Aymara', - '/languages/bug': 'Bugis', - '/languages/cel': 'Celtic (Other)', - '/languages/int': 'Interlingua (International Auxiliary Language Association)', - '/languages/xal': 'Oirat', - '/languages/ava': 'Avaric', - '/languages/son': 'Songhai', - '/languages/tah': 'Tahitian', - '/languages/tet': 'Tetum', - '/languages/ira': 'Iranian (Other)', - '/languages/kac': 'Kachin', - '/languages/nob': 'Norwegian (Bokmål)', - '/languages/vai': 'Vai', - '/languages/bik': 'Bikol', - '/languages/mos': 'Mooré', - '/languages/tig': 'Tigré', - '/languages/fat': 'Fanti', - '/languages/her': 'Herero', - '/languages/kal': 'Kalâtdlisut', - '/languages/mad': 'Madurese', - '/languages/yue': 'Cantonese', - '/languages/chn': 'Chinook jargon', - '/languages/hmn': 'Hmong', - '/languages/lin': 'Lingala', - '/languages/man': 'Mandingo', - '/languages/nds': 'Low German', - '/languages/bas': 'Basa', - '/languages/gay': 'Gayo', - '/languages/gsw': 'gsw', - '/languages/ine': 'Indo-European (Other)', - '/languages/kro': 'Kru (Other)', - '/languages/kum': 'Kumyk', - '/languages/tsi': 'Tsimshian', - '/languages/zap': 'Zapotec', - '/languages/ach': 'Acoli', - '/languages/ada': 'Adangme', - '/languages/aka': 'Akan', - '/languages/khi': 'Khoisan (Other)', - '/languages/srd': 'Sardinian', - '/languages/arn': 'Mapuche', - '/languages/dyu': 'Dyula', - '/languages/loz': 'Lozi', - '/languages/ltz': 'Luxembourgish', - '/languages/sag': 'Sango (Ubangi Creole)', - '/languages/lez': 'Lezgian', - '/languages/luo': 'Luo (Kenya and Tanzania)', - '/languages/ssw': 'Swazi ', - '/languages/krc': 'Karachay-Balkar', - '/languages/nyn': 'Nyankole', - '/languages/sal': 'Salishan languages', - '/languages/jpr': 'Judeo-Persian', - '/languages/pau': 'Palauan', - '/languages/smi': 'Sami', - '/languages/aar': 'Afar', - '/languages/abk': 'Abkhaz', - '/languages/gon': 'Gondi', - '/languages/nzi': 'Nzima', - '/languages/sam': 'Samaritan Aramaic', - '/languages/sao': 'Samoan', - '/languages/srr': 'Serer', - '/languages/apa': 'Apache languages', - '/languages/crh': 'Crimean Tatar', - '/languages/efi': 'Efik', - '/languages/iku': 'Inuktitut', - '/languages/nav': 'Navajo', - '/languages/pon': 'Ponape', - '/languages/tmh': 'Tamashek', - '/languages/aus': 'Australian languages', - '/languages/oto': 'Otomian languages', - '/languages/war': 'Waray', - '/languages/ypk': 'Yupik languages', - '/languages/ave': 'Avestan', - '/languages/cus': 'Cushitic (Other)', - '/languages/del': 'Delaware', - '/languages/fon': 'Fon', - '/languages/ina': 'Interlingua (International Auxiliary Language Association)', - '/languages/myv': 'Erzya', - '/languages/pag': 'Pangasinan', - '/languages/peo': 'Old Persian (ca. 600-400 B.C.)', - '/languages/vls': 'Flemish', - '/languages/bai': 'Bamileke languages', - '/languages/bla': 'Siksika', - '/languages/day': 'Dayak', - '/languages/men': 'Mende', - '/languages/tai': 'Tai', - '/languages/ton': 'Tongan', - '/languages/uga': 'Ugaritic', - '/languages/yao': 'Yao (Africa)', - '/languages/zza': 'Zaza', - '/languages/bin': 'Edo', - '/languages/frs': 'East Frisian', - '/languages/inh': 'Ingush', - '/languages/mah': 'Marshallese', - '/languages/sem': 'Semitic (Other)', - '/languages/art': 'Artificial (Other)', - '/languages/chy': 'Cheyenne', - '/languages/cmc': 'Chamic languages', - '/languages/dar': 'Dargwa', - '/languages/dua': 'Duala', - '/languages/elx': 'Elamite', - '/languages/fan': 'Fang', - '/languages/fij': 'Fijian', - '/languages/gil': 'Gilbertese', - '/languages/ijo': 'Ijo', - '/languages/kam': 'Kamba', - '/languages/nog': 'Nogai', - '/languages/non': 'Old Norse', - '/languages/tem': 'Temne', - '/languages/arg': 'Aragonese', - '/languages/arp': 'Arapaho', - '/languages/arw': 'Arawak', - '/languages/din': 'Dinka', - '/languages/grb': 'Grebo', - '/languages/kos': 'Kusaie', - '/languages/lub': 'Luba-Katanga', - '/languages/mnc': 'Manchu', - '/languages/nyo': 'Nyoro', - '/languages/rar': 'Rarotongan', - '/languages/sel': 'Selkup', - '/languages/tkl': 'Tokelauan', - '/languages/tog': 'Tonga (Nyasa)', - '/languages/tum': 'Tumbuka', - '/languages/alt': 'Altai', - '/languages/ase': 'American Sign Language', - '/languages/ast': 'Asturian', - '/languages/chk': 'Chuukese', - '/languages/cos': 'Corsican', - '/languages/ewo': 'Ewondo', - '/languages/gor': 'Gorontalo', - '/languages/hmo': 'Hiri Motu', - '/languages/lol': 'Mongo-Nkundu', - '/languages/lun': 'Lunda', - '/languages/mas': 'Masai', - '/languages/niu': 'Niuean', - '/languages/rup': 'Aromanian', - '/languages/sas': 'Sasak', - '/languages/sio': 'Siouan (Other)', - '/languages/sus': 'Susu', - '/languages/zun': 'Zuni', - '/languages/bat': 'Baltic (Other)', - '/languages/car': 'Carib', - '/languages/cha': 'Chamorro', - '/languages/kab': 'Kabyle', - '/languages/kau': 'Kanuri', - '/languages/kho': 'Khotanese', - '/languages/lua': 'Luba-Lulua', - '/languages/mdf': 'Moksha', - '/languages/nbl': 'Ndebele (South Africa)', - '/languages/umb': 'Umbundu', - '/languages/wak': 'Wakashan languages', - '/languages/wal': 'Wolayta', - '/languages/ale': 'Aleut', - '/languages/bis': 'Bislama', - '/languages/gba': 'Gbaya', - '/languages/glv': 'Manx', - '/languages/gul': 'Gullah', - '/languages/ipk': 'Inupiaq', - '/languages/krl': 'Karelian', - '/languages/lam': 'Lamba (Zambia and Congo)', - '/languages/sad': 'Sandawe', - '/languages/sid': 'Sidamo', - '/languages/snk': 'Soninke', - '/languages/srn': 'Sranan', - '/languages/suk': 'Sukuma', - '/languages/ter': 'Terena', - '/languages/tiv': 'Tiv', - '/languages/tli': 'Tlingit', - '/languages/tpi': 'Tok Pisin', - '/languages/tvl': 'Tuvaluan', - '/languages/yap': 'Yapese', - '/languages/eka': 'Ekajuk', - '/languages/hsb': 'Upper Sorbian', - '/languages/ido': 'Ido', - '/languages/kmb': 'Kimbundu', - '/languages/kpe': 'Kpelle', - '/languages/mwl': 'Mirandese', - '/languages/nno': 'Nynorsk', - '/languages/nub': 'Nubian languages', - '/languages/osa': 'Osage', - '/languages/sme': 'Northern Sami', - '/languages/znd': 'Zande languages', + "/languages/eng": "English", + "/languages/fre": "French", + "/languages/spa": "Spanish", + "/languages/ger": "German", + "/languages/rus": "Russian", + "/languages/ita": "Italian", + "/languages/chi": "Chinese", + "/languages/jpn": "Japanese", + "/languages/por": "Portuguese", + "/languages/ara": "Arabic", + "/languages/pol": "Polish", + "/languages/heb": "Hebrew", + "/languages/kor": "Korean", + "/languages/dut": "Dutch", + "/languages/ind": "Indonesian", + "/languages/lat": "Latin", + "/languages/und": "Undetermined", + "/languages/cmn": "Mandarin", + "/languages/hin": "Hindi", + "/languages/swe": "Swedish", + "/languages/dan": "Danish", + "/languages/urd": "Urdu", + "/languages/hun": "Hungarian", + "/languages/cze": "Czech", + "/languages/tur": "Turkish", + "/languages/ukr": "Ukrainian", + "/languages/gre": "Greek", + "/languages/vie": "Vietnamese", + "/languages/bul": "Bulgarian", + "/languages/ben": "Bengali", + "/languages/rum": "Romanian", + "/languages/cat": "Catalan", + "/languages/nor": "Norwegian", + "/languages/tha": "Thai", + "/languages/per": "Persian", + "/languages/scr": "Croatian", + "/languages/mul": "Multiple languages", + "/languages/fin": "Finnish", + "/languages/tam": "Tamil", + "/languages/guj": "Gujarati", + "/languages/mar": "Marathi", + "/languages/scc": "Serbian", + "/languages/pan": "Panjabi", + "/languages/wel": "Welsh", + "/languages/tel": "Telugu", + "/languages/yid": "Yiddish", + "/languages/kan": "Kannada", + "/languages/slo": "Slovak", + "/languages/san": "Sanskrit", + "/languages/arm": "Armenian", + "/languages/mal": "Malayalam", + "/languages/may": "Malay", + "/languages/bur": "Burmese", + "/languages/slv": "Slovenian", + "/languages/lit": "Lithuanian", + "/languages/tib": "Tibetan", + "/languages/lav": "Latvian", + "/languages/est": "Estonian", + "/languages/nep": "Nepali", + "/languages/ori": "Oriya", + "/languages/mon": "Mongolian", + "/languages/alb": "Albanian", + "/languages/iri": "Irish", + "/languages/geo": "Georgian", + "/languages/afr": "Afrikaans", + "/languages/grc": "Ancient Greek", + "/languages/mac": "Macedonian", + "/languages/bel": "Belarusian", + "/languages/ice": "Icelandic", + "/languages/srp": "Serbian", + "/languages/snh": "Sinhalese", + "/languages/snd": "Sindhi", + "/languages/ota": "Turkish, Ottoman", + "/languages/kur": "Kurdish", + "/languages/aze": "Azerbaijani", + "/languages/pus": "Pushto", + "/languages/amh": "Amharic", + "/languages/gag": "Galician", + "/languages/hrv": "Croatian", + "/languages/sin": "Sinhalese", + "/languages/asm": "Assamese", + "/languages/uzb": "Uzbek", + "/languages/gae": "Scottish Gaelix", + "/languages/kaz": "Kazakh", + "/languages/swa": "Swahili", + "/languages/bos": "Bosnian", + "/languages/glg": "Galician ", + "/languages/baq": "Basque", + "/languages/tgl": "Tagalog", + "/languages/raj": "Rajasthani", + "/languages/gle": "Irish", + "/languages/lao": "Lao", + "/languages/jav": "Javanese", + "/languages/mai": "Maithili", + "/languages/tgk": "Tajik ", + "/languages/khm": "Khmer", + "/languages/roh": "Raeto-Romance", + "/languages/kok": "Konkani ", + "/languages/sit": "Sino-Tibetan (Other)", + "/languages/mol": "Moldavian", + "/languages/kir": "Kyrgyz", + "/languages/new": "Newari", + "/languages/inc": "Indic (Other)", + "/languages/frm": "French, Middle (ca. 1300-1600)", + "/languages/esp": "Esperanto", + "/languages/hau": "Hausa", + "/languages/tag": "Tagalog", + "/languages/tuk": "Turkmen", + "/languages/enm": "English, Middle (1100-1500)", + "/languages/map": "Austronesian (Other)", + "/languages/pli": "Pali", + "/languages/fro": "French, Old (ca. 842-1300)", + "/languages/nic": "Niger-Kordofanian (Other)", + "/languages/tir": "Tigrinya", + "/languages/wen": "Sorbian (Other)", + "/languages/bho": "Bhojpuri", + "/languages/roa": "Romance (Other)", + "/languages/tut": "Altaic (Other)", + "/languages/bra": "Braj", + "/languages/sun": "Sundanese", + "/languages/fiu": "Finno-Ugrian (Other)", + "/languages/far": "Faroese", + "/languages/ban": "Balinese", + "/languages/tar": "Tatar", + "/languages/bak": "Bashkir", + "/languages/tat": "Tatar", + "/languages/chu": "Church Slavic", + "/languages/dra": "Dravidian (Other)", + "/languages/pra": "Prakrit languages", + "/languages/paa": "Papuan (Other)", + "/languages/doi": "Dogri", + "/languages/lah": "Lahndā", + "/languages/mni": "Manipuri", + "/languages/yor": "Yoruba", + "/languages/gmh": "German, Middle High (ca. 1050-1500)", + "/languages/kas": "Kashmiri", + "/languages/fri": "Frisian", + "/languages/mla": "Malagasy", + "/languages/egy": "Egyptian", + "/languages/rom": "Romani", + "/languages/syr": "Syriac, Modern", + "/languages/cau": "Caucasian (Other)", + "/languages/hbs": "Serbo-Croatian", + "/languages/sai": "South American Indian (Other)", + "/languages/pro": "Provençal (to 1500)", + "/languages/cpf": "Creoles and Pidgins, French-based (Other)", + "/languages/ang": "English, Old (ca. 450-1100)", + "/languages/bal": "Baluchi", + "/languages/gla": "Scottish Gaelic", + "/languages/chv": "Chuvash", + "/languages/kin": "Kinyarwanda", + "/languages/zul": "Zulu", + "/languages/sla": "Slavic (Other)", + "/languages/som": "Somali", + "/languages/mlt": "Maltese", + "/languages/uig": "Uighur", + "/languages/mlg": "Malagasy", + "/languages/sho": "Shona", + "/languages/lan": "Occitan (post 1500)", + "/languages/bre": "Breton", + "/languages/sco": "Scots", + "/languages/sso": "Sotho", + "/languages/myn": "Mayan languages", + "/languages/xho": "Xhosa", + "/languages/gem": "Germanic (Other)", + "/languages/esk": "Eskimo languages", + "/languages/akk": "Akkadian", + "/languages/div": "Maldivian", + "/languages/sah": "Yakut", + "/languages/tsw": "Tswana", + "/languages/nso": "Northern Sotho", + "/languages/pap": "Papiamento", + "/languages/bnt": "Bantu (Other)", + "/languages/oss": "Ossetic", + "/languages/cre": "Cree", + "/languages/ibo": "Igbo", + "/languages/fao": "Faroese", + "/languages/nai": "North American Indian (Other)", + "/languages/mag": "Magahi", + "/languages/arc": "Aramaic", + "/languages/epo": "Esperanto", + "/languages/kha": "Khasi", + "/languages/oji": "Ojibwa", + "/languages/que": "Quechua", + "/languages/lug": "Ganda", + "/languages/mwr": "Marwari", + "/languages/awa": "Awadhi ", + "/languages/cor": "Cornish", + "/languages/lad": "Ladino", + "/languages/dzo": "Dzongkha", + "/languages/cop": "Coptic", + "/languages/nah": "Nahuatl", + "/languages/cai": "Central American Indian (Other)", + "/languages/phi": "Philippine (Other)", + "/languages/moh": "Mohawk", + "/languages/crp": "Creoles and Pidgins (Other)", + "/languages/nya": "Nyanja", + "/languages/wol": "Wolof ", + "/languages/haw": "Hawaiian", + "/languages/eth": "Ethiopic", + "/languages/mis": "Miscellaneous languages", + "/languages/mkh": "Mon-Khmer (Other)", + "/languages/alg": "Algonquian (Other)", + "/languages/nde": "Ndebele (Zimbabwe)", + "/languages/ssa": "Nilo-Saharan (Other)", + "/languages/chm": "Mari", + "/languages/che": "Chechen", + "/languages/gez": "Ethiopic", + "/languages/ven": "Venda", + "/languages/cam": "Khmer", + "/languages/fur": "Friulian", + "/languages/ful": "Fula", + "/languages/gal": "Oromo", + "/languages/jrb": "Judeo-Arabic", + "/languages/bua": "Buriat", + "/languages/ady": "Adygei", + "/languages/bem": "Bemba", + "/languages/kar": "Karen languages", + "/languages/sna": "Shona", + "/languages/twi": "Twi", + "/languages/btk": "Batak", + "/languages/kaa": "Kara-Kalpak", + "/languages/kom": "Komi", + "/languages/sot": "Sotho", + "/languages/tso": "Tsonga", + "/languages/cpe": "Creoles and Pidgins, English-based (Other)", + "/languages/gua": "Guarani", + "/languages/mao": "Maori", + "/languages/mic": "Micmac", + "/languages/swz": "Swazi", + "/languages/taj": "Tajik", + "/languages/smo": "Samoan", + "/languages/ace": "Achinese", + "/languages/afa": "Afroasiatic (Other)", + "/languages/lap": "Sami", + "/languages/min": "Minangkabau", + "/languages/oci": "Occitan (post 1500)", + "/languages/tsn": "Tswana", + "/languages/pal": "Pahlavi", + "/languages/sux": "Sumerian", + "/languages/ewe": "Ewe", + "/languages/him": "Himachali", + "/languages/kaw": "Kawi", + "/languages/lus": "Lushai", + "/languages/ceb": "Cebuano", + "/languages/chr": "Cherokee", + "/languages/fil": "Filipino", + "/languages/ndo": "Ndonga", + "/languages/ilo": "Iloko", + "/languages/kbd": "Kabardian", + "/languages/orm": "Oromo", + "/languages/dum": "Dutch, Middle (ca. 1050-1350)", + "/languages/bam": "Bambara", + "/languages/goh": "Old High German", + "/languages/got": "Gothic", + "/languages/kon": "Kongo", + "/languages/mun": "Munda (Other)", + "/languages/kru": "Kurukh", + "/languages/pam": "Pampanga", + "/languages/grn": "Guarani", + "/languages/gaa": "Gã", + "/languages/fry": "Frisian", + "/languages/iba": "Iban", + "/languages/mak": "Makasar", + "/languages/kik": "Kikuyu", + "/languages/cho": "Choctaw", + "/languages/cpp": "Creoles and Pidgins, Portuguese-based (Other)", + "/languages/dak": "Dakota", + "/languages/udm": "Udmurt ", + "/languages/hat": "Haitian French Creole", + "/languages/mus": "Creek", + "/languages/ber": "Berber (Other)", + "/languages/hil": "Hiligaynon", + "/languages/iro": "Iroquoian (Other)", + "/languages/kua": "Kuanyama", + "/languages/mno": "Manobo languages", + "/languages/run": "Rundi", + "/languages/sat": "Santali", + "/languages/shn": "Shan", + "/languages/tyv": "Tuvinian", + "/languages/chg": "Chagatai", + "/languages/syc": "Syriac", + "/languages/ath": "Athapascan (Other)", + "/languages/aym": "Aymara", + "/languages/bug": "Bugis", + "/languages/cel": "Celtic (Other)", + "/languages/int": "Interlingua (International Auxiliary Language Association)", + "/languages/xal": "Oirat", + "/languages/ava": "Avaric", + "/languages/son": "Songhai", + "/languages/tah": "Tahitian", + "/languages/tet": "Tetum", + "/languages/ira": "Iranian (Other)", + "/languages/kac": "Kachin", + "/languages/nob": "Norwegian (Bokmål)", + "/languages/vai": "Vai", + "/languages/bik": "Bikol", + "/languages/mos": "Mooré", + "/languages/tig": "Tigré", + "/languages/fat": "Fanti", + "/languages/her": "Herero", + "/languages/kal": "Kalâtdlisut", + "/languages/mad": "Madurese", + "/languages/yue": "Cantonese", + "/languages/chn": "Chinook jargon", + "/languages/hmn": "Hmong", + "/languages/lin": "Lingala", + "/languages/man": "Mandingo", + "/languages/nds": "Low German", + "/languages/bas": "Basa", + "/languages/gay": "Gayo", + "/languages/gsw": "gsw", + "/languages/ine": "Indo-European (Other)", + "/languages/kro": "Kru (Other)", + "/languages/kum": "Kumyk", + "/languages/tsi": "Tsimshian", + "/languages/zap": "Zapotec", + "/languages/ach": "Acoli", + "/languages/ada": "Adangme", + "/languages/aka": "Akan", + "/languages/khi": "Khoisan (Other)", + "/languages/srd": "Sardinian", + "/languages/arn": "Mapuche", + "/languages/dyu": "Dyula", + "/languages/loz": "Lozi", + "/languages/ltz": "Luxembourgish", + "/languages/sag": "Sango (Ubangi Creole)", + "/languages/lez": "Lezgian", + "/languages/luo": "Luo (Kenya and Tanzania)", + "/languages/ssw": "Swazi ", + "/languages/krc": "Karachay-Balkar", + "/languages/nyn": "Nyankole", + "/languages/sal": "Salishan languages", + "/languages/jpr": "Judeo-Persian", + "/languages/pau": "Palauan", + "/languages/smi": "Sami", + "/languages/aar": "Afar", + "/languages/abk": "Abkhaz", + "/languages/gon": "Gondi", + "/languages/nzi": "Nzima", + "/languages/sam": "Samaritan Aramaic", + "/languages/sao": "Samoan", + "/languages/srr": "Serer", + "/languages/apa": "Apache languages", + "/languages/crh": "Crimean Tatar", + "/languages/efi": "Efik", + "/languages/iku": "Inuktitut", + "/languages/nav": "Navajo", + "/languages/pon": "Ponape", + "/languages/tmh": "Tamashek", + "/languages/aus": "Australian languages", + "/languages/oto": "Otomian languages", + "/languages/war": "Waray", + "/languages/ypk": "Yupik languages", + "/languages/ave": "Avestan", + "/languages/cus": "Cushitic (Other)", + "/languages/del": "Delaware", + "/languages/fon": "Fon", + "/languages/ina": "Interlingua (International Auxiliary Language Association)", + "/languages/myv": "Erzya", + "/languages/pag": "Pangasinan", + "/languages/peo": "Old Persian (ca. 600-400 B.C.)", + "/languages/vls": "Flemish", + "/languages/bai": "Bamileke languages", + "/languages/bla": "Siksika", + "/languages/day": "Dayak", + "/languages/men": "Mende", + "/languages/tai": "Tai", + "/languages/ton": "Tongan", + "/languages/uga": "Ugaritic", + "/languages/yao": "Yao (Africa)", + "/languages/zza": "Zaza", + "/languages/bin": "Edo", + "/languages/frs": "East Frisian", + "/languages/inh": "Ingush", + "/languages/mah": "Marshallese", + "/languages/sem": "Semitic (Other)", + "/languages/art": "Artificial (Other)", + "/languages/chy": "Cheyenne", + "/languages/cmc": "Chamic languages", + "/languages/dar": "Dargwa", + "/languages/dua": "Duala", + "/languages/elx": "Elamite", + "/languages/fan": "Fang", + "/languages/fij": "Fijian", + "/languages/gil": "Gilbertese", + "/languages/ijo": "Ijo", + "/languages/kam": "Kamba", + "/languages/nog": "Nogai", + "/languages/non": "Old Norse", + "/languages/tem": "Temne", + "/languages/arg": "Aragonese", + "/languages/arp": "Arapaho", + "/languages/arw": "Arawak", + "/languages/din": "Dinka", + "/languages/grb": "Grebo", + "/languages/kos": "Kusaie", + "/languages/lub": "Luba-Katanga", + "/languages/mnc": "Manchu", + "/languages/nyo": "Nyoro", + "/languages/rar": "Rarotongan", + "/languages/sel": "Selkup", + "/languages/tkl": "Tokelauan", + "/languages/tog": "Tonga (Nyasa)", + "/languages/tum": "Tumbuka", + "/languages/alt": "Altai", + "/languages/ase": "American Sign Language", + "/languages/ast": "Asturian", + "/languages/chk": "Chuukese", + "/languages/cos": "Corsican", + "/languages/ewo": "Ewondo", + "/languages/gor": "Gorontalo", + "/languages/hmo": "Hiri Motu", + "/languages/lol": "Mongo-Nkundu", + "/languages/lun": "Lunda", + "/languages/mas": "Masai", + "/languages/niu": "Niuean", + "/languages/rup": "Aromanian", + "/languages/sas": "Sasak", + "/languages/sio": "Siouan (Other)", + "/languages/sus": "Susu", + "/languages/zun": "Zuni", + "/languages/bat": "Baltic (Other)", + "/languages/car": "Carib", + "/languages/cha": "Chamorro", + "/languages/kab": "Kabyle", + "/languages/kau": "Kanuri", + "/languages/kho": "Khotanese", + "/languages/lua": "Luba-Lulua", + "/languages/mdf": "Moksha", + "/languages/nbl": "Ndebele (South Africa)", + "/languages/umb": "Umbundu", + "/languages/wak": "Wakashan languages", + "/languages/wal": "Wolayta", + "/languages/ale": "Aleut", + "/languages/bis": "Bislama", + "/languages/gba": "Gbaya", + "/languages/glv": "Manx", + "/languages/gul": "Gullah", + "/languages/ipk": "Inupiaq", + "/languages/krl": "Karelian", + "/languages/lam": "Lamba (Zambia and Congo)", + "/languages/sad": "Sandawe", + "/languages/sid": "Sidamo", + "/languages/snk": "Soninke", + "/languages/srn": "Sranan", + "/languages/suk": "Sukuma", + "/languages/ter": "Terena", + "/languages/tiv": "Tiv", + "/languages/tli": "Tlingit", + "/languages/tpi": "Tok Pisin", + "/languages/tvl": "Tuvaluan", + "/languages/yap": "Yapese", + "/languages/eka": "Ekajuk", + "/languages/hsb": "Upper Sorbian", + "/languages/ido": "Ido", + "/languages/kmb": "Kimbundu", + "/languages/kpe": "Kpelle", + "/languages/mwl": "Mirandese", + "/languages/nno": "Nynorsk", + "/languages/nub": "Nubian languages", + "/languages/osa": "Osage", + "/languages/sme": "Northern Sami", + "/languages/znd": "Zande languages", } diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index b3a4d6f9f..60acb59bd 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -1,4 +1,4 @@ -''' using a bookwyrm instance as a source of book data ''' +""" using a bookwyrm instance as a source of book data """ from functools import reduce import operator @@ -10,10 +10,11 @@ from .abstract_connector import AbstractConnector, SearchResult class Connector(AbstractConnector): - ''' instantiate a connector ''' + """ instantiate a connector """ + # pylint: disable=arguments-differ def search(self, query, min_confidence=0.1, raw=False): - ''' search your local database ''' + """ search your local database """ if not query: return [] # first, try searching unqiue identifiers @@ -34,19 +35,18 @@ class Connector(AbstractConnector): return search_results def isbn_search(self, query, raw=False): - ''' search your local database ''' + """ search your local database """ if not query: return [] - filters = [{f: query} for f in ['isbn_10', 'isbn_13']] + filters = [{f: query} for f in ["isbn_10", "isbn_13"]] results = models.Edition.objects.filter( reduce(operator.or_, (Q(**f) for f in filters)) ).distinct() # when there are multiple editions of the same work, pick the default. # it would be odd for this to happen. - results = results.filter(parent_work__default_edition__id=F('id')) \ - or results + results = results.filter(parent_work__default_edition__id=F("id")) or results search_results = [] for result in results: @@ -58,33 +58,30 @@ class Connector(AbstractConnector): break return search_results - def format_search_result(self, search_result): return SearchResult( title=search_result.title, key=search_result.remote_id, author=search_result.author_text, - year=search_result.published_date.year if \ - search_result.published_date else None, + year=search_result.published_date.year + if search_result.published_date + else None, connector=self, - confidence=search_result.rank if \ - hasattr(search_result, 'rank') else 1, + confidence=search_result.rank if hasattr(search_result, "rank") else 1, ) - def format_isbn_search_result(self, search_result): return SearchResult( title=search_result.title, key=search_result.remote_id, author=search_result.author_text, - year=search_result.published_date.year if \ - search_result.published_date else None, + year=search_result.published_date.year + if search_result.published_date + else None, connector=self, - confidence=search_result.rank if \ - hasattr(search_result, 'rank') else 1, + confidence=search_result.rank if hasattr(search_result, "rank") else 1, ) - def is_work_data(self, data): pass @@ -98,11 +95,11 @@ class Connector(AbstractConnector): return None def parse_isbn_search_data(self, data): - ''' it's already in the right format, don't even worry about it ''' + """ it's already in the right format, don't even worry about it """ return data def parse_search_data(self, data): - ''' it's already in the right format, don't even worry about it ''' + """ it's already in the right format, don't even worry about it """ return data def expand_book_data(self, book): @@ -110,44 +107,47 @@ class Connector(AbstractConnector): def search_identifiers(query): - ''' tries remote_id, isbn; defined as dedupe fields on the model ''' - filters = [{f.name: query} for f in models.Edition._meta.get_fields() \ - if hasattr(f, 'deduplication_field') and f.deduplication_field] + """ tries remote_id, isbn; defined as dedupe fields on the model """ + filters = [ + {f.name: query} + for f in models.Edition._meta.get_fields() + if hasattr(f, "deduplication_field") and f.deduplication_field + ] results = models.Edition.objects.filter( reduce(operator.or_, (Q(**f) for f in filters)) ).distinct() # when there are multiple editions of the same work, pick the default. # it would be odd for this to happen. - return results.filter(parent_work__default_edition__id=F('id')) \ - or results + return results.filter(parent_work__default_edition__id=F("id")) or results def search_title_author(query, min_confidence): - ''' searches for title and author ''' - vector = SearchVector('title', weight='A') +\ - SearchVector('subtitle', weight='B') +\ - SearchVector('authors__name', weight='C') +\ - SearchVector('series', weight='D') + """ searches for title and author """ + vector = ( + SearchVector("title", weight="A") + + SearchVector("subtitle", weight="B") + + SearchVector("authors__name", weight="C") + + SearchVector("series", weight="D") + ) - results = models.Edition.objects.annotate( - search=vector - ).annotate( - rank=SearchRank(vector, query) - ).filter( - rank__gt=min_confidence - ).order_by('-rank') + results = ( + models.Edition.objects.annotate(search=vector) + .annotate(rank=SearchRank(vector, query)) + .filter(rank__gt=min_confidence) + .order_by("-rank") + ) # when there are multiple editions of the same work, pick the closest - editions_of_work = results.values( - 'parent_work' - ).annotate( - Count('parent_work') - ).values_list('parent_work') + editions_of_work = ( + results.values("parent_work") + .annotate(Count("parent_work")) + .values_list("parent_work") + ) for work_id in set(editions_of_work): editions = results.filter(parent_work=work_id) - default = editions.filter(parent_work__default_edition=F('id')) + default = editions.filter(parent_work__default_edition=F("id")) default_rank = default.first().rank if default.exists() else 0 # if mutliple books have the top rank, pick the default edition if default_rank == editions.first().rank: diff --git a/bookwyrm/connectors/settings.py b/bookwyrm/connectors/settings.py index e04aedeff..f1674cf7c 100644 --- a/bookwyrm/connectors/settings.py +++ b/bookwyrm/connectors/settings.py @@ -1,3 +1,3 @@ -''' settings book data connectors ''' +""" settings book data connectors """ -CONNECTORS = ['openlibrary', 'self_connector', 'bookwyrm_connector'] +CONNECTORS = ["openlibrary", "self_connector", "bookwyrm_connector"] diff --git a/bookwyrm/context_processors.py b/bookwyrm/context_processors.py index a1471ac48..8f79a6529 100644 --- a/bookwyrm/context_processors.py +++ b/bookwyrm/context_processors.py @@ -1,8 +1,7 @@ -''' customize the info available in context for rendering templates ''' +""" customize the info available in context for rendering templates """ from bookwyrm import models -def site_settings(request):# pylint: disable=unused-argument - ''' include the custom info about the site ''' - return { - 'site': models.SiteSettings.objects.get() - } + +def site_settings(request): # pylint: disable=unused-argument + """ include the custom info about the site """ + return {"site": models.SiteSettings.objects.get()} diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 2319d4677..c7536876d 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -1,25 +1,27 @@ -''' send emails ''' +""" send emails """ from django.core.mail import send_mail from bookwyrm import models from bookwyrm.tasks import app + def password_reset_email(reset_code): - ''' generate a password reset email ''' + """ generate a password reset email """ site = models.SiteSettings.get() send_email.delay( reset_code.user.email, - 'Reset your password on %s' % site.name, - 'Your password reset link: %s' % reset_code.link + "Reset your password on %s" % site.name, + "Your password reset link: %s" % reset_code.link, ) + @app.task def send_email(recipient, subject, message): - ''' use a task to send the email ''' + """ use a task to send the email """ send_mail( subject, message, - None, # sender will be the config default + None, # sender will be the config default [recipient], - fail_silently=False + fail_silently=False, ) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index b920fc9c0..3d8839efa 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -1,4 +1,4 @@ -''' using django model forms ''' +""" using django model forms """ import datetime from collections import defaultdict @@ -12,115 +12,125 @@ from bookwyrm import models class CustomForm(ModelForm): - ''' add css classes to the forms ''' + """ add css classes to the forms """ + def __init__(self, *args, **kwargs): - css_classes = defaultdict(lambda: '') - css_classes['text'] = 'input' - css_classes['password'] = 'input' - css_classes['email'] = 'input' - css_classes['number'] = 'input' - css_classes['checkbox'] = 'checkbox' - css_classes['textarea'] = 'textarea' + css_classes = defaultdict(lambda: "") + css_classes["text"] = "input" + css_classes["password"] = "input" + css_classes["email"] = "input" + css_classes["number"] = "input" + css_classes["checkbox"] = "checkbox" + css_classes["textarea"] = "textarea" super(CustomForm, self).__init__(*args, **kwargs) for visible in self.visible_fields(): - if hasattr(visible.field.widget, 'input_type'): + if hasattr(visible.field.widget, "input_type"): 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['class'] = css_classes[input_type] + input_type = "textarea" + visible.field.widget.attrs["cols"] = None + visible.field.widget.attrs["rows"] = None + visible.field.widget.attrs["class"] = css_classes[input_type] # pylint: disable=missing-class-docstring class LoginForm(CustomForm): class Meta: model = models.User - fields = ['localname', 'password'] + fields = ["localname", "password"] help_texts = {f: None for f in fields} widgets = { - 'password': PasswordInput(), + "password": PasswordInput(), } class RegisterForm(CustomForm): class Meta: model = models.User - fields = ['localname', 'email', 'password'] + fields = ["localname", "email", "password"] help_texts = {f: None for f in fields} - widgets = { - 'password': PasswordInput() - } + widgets = {"password": PasswordInput()} class RatingForm(CustomForm): class Meta: model = models.Review - fields = ['user', 'book', 'content', 'rating', 'privacy'] + fields = ["user", "book", "content", "rating", "privacy"] class ReviewForm(CustomForm): class Meta: model = models.Review fields = [ - 'user', 'book', - 'name', 'content', 'rating', - 'content_warning', 'sensitive', - 'privacy'] + "user", + "book", + "name", + "content", + "rating", + "content_warning", + "sensitive", + "privacy", + ] class CommentForm(CustomForm): class Meta: model = models.Comment - fields = [ - 'user', 'book', 'content', - 'content_warning', 'sensitive', - 'privacy'] + fields = ["user", "book", "content", "content_warning", "sensitive", "privacy"] class QuotationForm(CustomForm): class Meta: model = models.Quotation fields = [ - 'user', 'book', 'quote', 'content', - 'content_warning', 'sensitive', 'privacy'] + "user", + "book", + "quote", + "content", + "content_warning", + "sensitive", + "privacy", + ] class ReplyForm(CustomForm): class Meta: model = models.Status fields = [ - 'user', 'content', 'content_warning', 'sensitive', - 'reply_parent', 'privacy'] + "user", + "content", + "content_warning", + "sensitive", + "reply_parent", + "privacy", + ] + class StatusForm(CustomForm): class Meta: model = models.Status - fields = [ - 'user', 'content', 'content_warning', 'sensitive', 'privacy'] + fields = ["user", "content", "content_warning", "sensitive", "privacy"] class EditUserForm(CustomForm): class Meta: model = models.User - fields = [ - 'avatar', 'name', 'email', 'summary', 'manually_approves_followers' - ] + fields = ["avatar", "name", "email", "summary", "manually_approves_followers"] help_texts = {f: None for f in fields} class TagForm(CustomForm): class Meta: model = models.Tag - fields = ['name'] + fields = ["name"] help_texts = {f: None for f in fields} - labels = {'name': 'Add a tag'} + labels = {"name": "Add a tag"} class CoverForm(CustomForm): class Meta: model = models.Book - fields = ['cover'] + fields = ["cover"] help_texts = {f: None for f in fields} @@ -128,80 +138,87 @@ class EditionForm(CustomForm): class Meta: model = models.Edition exclude = [ - 'remote_id', - 'origin_id', - 'created_date', - 'updated_date', - 'edition_rank', - - 'authors',# TODO - 'parent_work', - 'shelves', - - 'subjects',# TODO - 'subject_places',# TODO - - 'connector', + "remote_id", + "origin_id", + "created_date", + "updated_date", + "edition_rank", + "authors", # TODO + "parent_work", + "shelves", + "subjects", # TODO + "subject_places", # TODO + "connector", ] + class AuthorForm(CustomForm): class Meta: model = models.Author exclude = [ - 'remote_id', - 'origin_id', - 'created_date', - 'updated_date', + "remote_id", + "origin_id", + "created_date", + "updated_date", ] class ImportForm(forms.Form): csv_file = forms.FileField() + class ExpiryWidget(widgets.Select): def value_from_datadict(self, data, files, name): - ''' human-readable exiration time buckets ''' + """ human-readable exiration time buckets """ selected_string = super().value_from_datadict(data, files, name) - if selected_string == 'day': + if selected_string == "day": interval = datetime.timedelta(days=1) - elif selected_string == 'week': + elif selected_string == "week": interval = datetime.timedelta(days=7) - elif selected_string == 'month': - interval = datetime.timedelta(days=31) # Close enough? - elif selected_string == 'forever': + elif selected_string == "month": + interval = datetime.timedelta(days=31) # Close enough? + elif selected_string == "forever": return None else: - return selected_string # "This will raise + return selected_string # "This will raise return timezone.now() + interval + class CreateInviteForm(CustomForm): class Meta: model = models.SiteInvite - exclude = ['code', 'user', 'times_used'] + exclude = ["code", "user", "times_used"] widgets = { - 'expiry': ExpiryWidget(choices=[ - ('day', _('One Day')), - ('week', _('One Week')), - ('month', _('One Month')), - ('forever', _('Does Not Expire'))]), - 'use_limit': widgets.Select( - choices=[(i, _("%(count)d uses" % {'count': i})) \ - for i in [1, 5, 10, 25, 50, 100]] - + [(None, _('Unlimited'))]) + "expiry": ExpiryWidget( + choices=[ + ("day", _("One Day")), + ("week", _("One Week")), + ("month", _("One Month")), + ("forever", _("Does Not Expire")), + ] + ), + "use_limit": widgets.Select( + choices=[ + (i, _("%(count)d uses" % {"count": i})) + for i in [1, 5, 10, 25, 50, 100] + ] + + [(None, _("Unlimited"))] + ), } + class ShelfForm(CustomForm): class Meta: model = models.Shelf - fields = ['user', 'name', 'privacy'] + fields = ["user", "name", "privacy"] class GoalForm(CustomForm): class Meta: model = models.AnnualGoal - fields = ['user', 'year', 'goal', 'privacy'] + fields = ["user", "year", "goal", "privacy"] class SiteForm(CustomForm): @@ -213,4 +230,4 @@ class SiteForm(CustomForm): class ListForm(CustomForm): class Meta: model = models.List - fields = ['user', 'name', 'description', 'curation', 'privacy'] + fields = ["user", "name", "description", "curation", "privacy"] diff --git a/bookwyrm/goodreads_import.py b/bookwyrm/goodreads_import.py index f5b84e179..fb4e8e0f1 100644 --- a/bookwyrm/goodreads_import.py +++ b/bookwyrm/goodreads_import.py @@ -1,13 +1,14 @@ -''' handle reading a csv from goodreads ''' +""" handle reading a csv from goodreads """ from bookwyrm.importer import Importer -# GoodReads is the default importer, thus Importer follows its structure. For a more complete example of overriding see librarything_import.py +# GoodReads is the default importer, thus Importer follows its structure. For a more complete example of overriding see librarything_import.py + class GoodreadsImporter(Importer): - service = 'GoodReads' + service = "GoodReads" def parse_fields(self, data): - data.update({'import_source': self.service }) + data.update({"import_source": self.service}) # add missing 'Date Started' field - data.update({'Date Started': None }) + data.update({"Date Started": None}) return data diff --git a/bookwyrm/importer.py b/bookwyrm/importer.py index a12884007..2fbb3430f 100644 --- a/bookwyrm/importer.py +++ b/bookwyrm/importer.py @@ -1,4 +1,4 @@ -''' handle reading a csv from an external service, defaults are from GoodReads ''' +""" handle reading a csv from an external service, defaults are from GoodReads """ import csv import logging @@ -8,49 +8,48 @@ from bookwyrm.tasks import app logger = logging.getLogger(__name__) + class Importer: - service = 'Unknown' - delimiter = ',' - encoding = 'UTF-8' - mandatory_fields = ['Title', 'Author'] + service = "Unknown" + delimiter = "," + encoding = "UTF-8" + mandatory_fields = ["Title", "Author"] def create_job(self, user, csv_file, include_reviews, privacy): - ''' check over a csv and creates a database entry for the job''' + """ check over a csv and creates a database entry for the job""" job = ImportJob.objects.create( - user=user, - include_reviews=include_reviews, - privacy=privacy + user=user, include_reviews=include_reviews, privacy=privacy ) - for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.delimiter ))): + for index, entry in enumerate( + list(csv.DictReader(csv_file, delimiter=self.delimiter)) + ): if not all(x in entry for x in self.mandatory_fields): - raise ValueError('Author and title must be in data.') + raise ValueError("Author and title must be in data.") entry = self.parse_fields(entry) self.save_item(job, index, entry) return job - def save_item(self, job, index, data): ImportItem(job=job, index=index, data=data).save() def parse_fields(self, entry): - entry.update({'import_source': self.service }) - return entry + entry.update({"import_source": self.service}) + return entry def create_retry_job(self, user, original_job, items): - ''' retry items that didn't import ''' + """ retry items that didn't import """ job = ImportJob.objects.create( user=user, include_reviews=original_job.include_reviews, privacy=original_job.privacy, - retry=True + retry=True, ) for item in items: self.save_item(job, item.index, item.data) return job - def start_import(self, job): - ''' initalizes a csv import job ''' + """ initalizes a csv import job """ result = import_data.delay(self.service, job.id) job.task_id = result.id job.save() @@ -58,15 +57,15 @@ class Importer: @app.task def import_data(source, job_id): - ''' does the actual lookup work in a celery task ''' + """ does the actual lookup work in a celery task """ job = ImportJob.objects.get(id=job_id) try: for item in job.items.all(): try: item.resolve() - except Exception as e:# pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.exception(e) - item.fail_reason = 'Error loading book' + item.fail_reason = "Error loading book" item.save() continue @@ -74,10 +73,11 @@ def import_data(source, job_id): item.save() # shelves book and handles reviews - handle_imported_book(source, - job.user, item, job.include_reviews, job.privacy) + handle_imported_book( + source, job.user, item, job.include_reviews, job.privacy + ) else: - item.fail_reason = 'Could not find a match for book' + item.fail_reason = "Could not find a match for book" item.save() finally: job.complete = True @@ -85,41 +85,41 @@ def import_data(source, job_id): def handle_imported_book(source, user, item, include_reviews, privacy): - ''' process a csv and then post about it ''' + """ process a csv and then post about it """ if isinstance(item.book, models.Work): item.book = item.book.default_edition if not item.book: return - existing_shelf = models.ShelfBook.objects.filter( - book=item.book, user=user).exists() + existing_shelf = models.ShelfBook.objects.filter(book=item.book, user=user).exists() # shelve the book if it hasn't been shelved already if item.shelf and not existing_shelf: - desired_shelf = models.Shelf.objects.get( - identifier=item.shelf, - user=user - ) - models.ShelfBook.objects.create( - book=item.book, shelf=desired_shelf, user=user) + desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user) + models.ShelfBook.objects.create(book=item.book, shelf=desired_shelf, user=user) for read in item.reads: # check for an existing readthrough with the same dates if models.ReadThrough.objects.filter( - user=user, book=item.book, - start_date=read.start_date, - finish_date=read.finish_date - ).exists(): + user=user, + book=item.book, + start_date=read.start_date, + finish_date=read.finish_date, + ).exists(): continue read.book = item.book read.user = user read.save() if include_reviews and (item.rating or item.review): - review_title = 'Review of {!r} on {!r}'.format( - item.book.title, - source, - ) if item.review else '' + review_title = ( + "Review of {!r} on {!r}".format( + item.book.title, + source, + ) + if item.review + else "" + ) # we don't know the publication date of the review, # but "now" is a bad guess diff --git a/bookwyrm/librarything_import.py b/bookwyrm/librarything_import.py index 0584daad9..b3dd9d56b 100644 --- a/bookwyrm/librarything_import.py +++ b/bookwyrm/librarything_import.py @@ -1,4 +1,4 @@ -''' handle reading a csv from librarything ''' +""" handle reading a csv from librarything """ import csv import re import math @@ -9,34 +9,34 @@ from bookwyrm.importer import Importer class LibrarythingImporter(Importer): - service = 'LibraryThing' - delimiter = '\t' - encoding = 'ISO-8859-1' + service = "LibraryThing" + delimiter = "\t" + encoding = "ISO-8859-1" # mandatory_fields : fields matching the book title and author - mandatory_fields = ['Title', 'Primary Author'] + mandatory_fields = ["Title", "Primary Author"] def parse_fields(self, initial): data = {} - data['import_source'] = self.service - data['Book Id'] = initial['Book Id'] - data['Title'] = initial['Title'] - data['Author'] = initial['Primary Author'] - data['ISBN13'] = initial['ISBN'] - data['My Review'] = initial['Review'] - if initial['Rating']: - data['My Rating'] = math.ceil(float(initial['Rating'])) + data["import_source"] = self.service + data["Book Id"] = initial["Book Id"] + data["Title"] = initial["Title"] + data["Author"] = initial["Primary Author"] + data["ISBN13"] = initial["ISBN"] + data["My Review"] = initial["Review"] + if initial["Rating"]: + data["My Rating"] = math.ceil(float(initial["Rating"])) else: - data['My Rating'] = '' - data['Date Added'] = re.sub('\[|\]', '', initial['Entry Date']) - data['Date Started'] = re.sub('\[|\]', '', initial['Date Started']) - data['Date Read'] = re.sub('\[|\]', '', initial['Date Read']) + data["My Rating"] = "" + data["Date Added"] = re.sub("\[|\]", "", initial["Entry Date"]) + data["Date Started"] = re.sub("\[|\]", "", initial["Date Started"]) + data["Date Read"] = re.sub("\[|\]", "", initial["Date Read"]) - data['Exclusive Shelf'] = None - if data['Date Read']: - data['Exclusive Shelf'] = "read" - elif data['Date Started']: - data['Exclusive Shelf'] = "reading" + data["Exclusive Shelf"] = None + if data["Date Read"]: + data["Exclusive Shelf"] = "read" + elif data["Date Started"]: + data["Exclusive Shelf"] = "reading" else: - data['Exclusive Shelf'] = "to-read" + data["Exclusive Shelf"] = "to-read" return data diff --git a/bookwyrm/management/commands/deduplicate_book_data.py b/bookwyrm/management/commands/deduplicate_book_data.py index 044b2a986..edd91a717 100644 --- a/bookwyrm/management/commands/deduplicate_book_data.py +++ b/bookwyrm/management/commands/deduplicate_book_data.py @@ -1,26 +1,20 @@ -''' PROCEED WITH CAUTION: uses deduplication fields to permanently -merge book data objects ''' +""" PROCEED WITH CAUTION: uses deduplication fields to permanently +merge book data objects """ from django.core.management.base import BaseCommand from django.db.models import Count from bookwyrm import models def update_related(canonical, obj): - ''' update all the models with fk to the object being removed ''' + """ update all the models with fk to the object being removed """ # move related models to canonical related_models = [ - (r.remote_field.name, r.related_model) for r in \ - canonical._meta.related_objects] + (r.remote_field.name, r.related_model) for r in canonical._meta.related_objects + ] for (related_field, related_model) in related_models: - related_objs = related_model.objects.filter( - **{related_field: obj}) + related_objs = related_model.objects.filter(**{related_field: obj}) for related_obj in related_objs: - print( - 'replacing in', - related_model.__name__, - related_field, - related_obj.id - ) + print("replacing in", related_model.__name__, related_field, related_obj.id) try: setattr(related_obj, related_field, canonical) related_obj.save() @@ -30,40 +24,41 @@ def update_related(canonical, obj): def copy_data(canonical, obj): - ''' try to get the most data possible ''' + """ try to get the most data possible """ for data_field in obj._meta.get_fields(): - if not hasattr(data_field, 'activitypub_field'): + if not hasattr(data_field, "activitypub_field"): continue data_value = getattr(obj, data_field.name) if not data_value: continue if not getattr(canonical, data_field.name): - print('setting data field', data_field.name, data_value) + print("setting data field", data_field.name, data_value) setattr(canonical, data_field.name, data_value) canonical.save() def dedupe_model(model): - ''' combine duplicate editions and update related models ''' + """ combine duplicate editions and update related models """ fields = model._meta.get_fields() - dedupe_fields = [f for f in fields if \ - hasattr(f, 'deduplication_field') and f.deduplication_field] + dedupe_fields = [ + f for f in fields if hasattr(f, "deduplication_field") and f.deduplication_field + ] for field in dedupe_fields: - dupes = model.objects.values(field.name).annotate( - Count(field.name) - ).filter(**{'%s__count__gt' % field.name: 1}) + dupes = ( + model.objects.values(field.name) + .annotate(Count(field.name)) + .filter(**{"%s__count__gt" % field.name: 1}) + ) for dupe in dupes: value = dupe[field.name] - if not value or value == '': + if not value or value == "": continue - print('----------') + print("----------") print(dupe) - objs = model.objects.filter( - **{field.name: value} - ).order_by('id') + objs = model.objects.filter(**{field.name: value}).order_by("id") canonical = objs.first() - print('keeping', canonical.remote_id) + print("keeping", canonical.remote_id) for obj in objs[1:]: print(obj.remote_id) copy_data(canonical, obj) @@ -73,11 +68,12 @@ def dedupe_model(model): class Command(BaseCommand): - ''' dedplucate allllll the book data models ''' - help = 'merges duplicate book data' + """ dedplucate allllll the book data models """ + + help = "merges duplicate book data" # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): - ''' run deudplications ''' + """ run deudplications """ dedupe_model(models.Edition) dedupe_model(models.Work) dedupe_model(models.Author) diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index 5759abfcc..6b3f3762e 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -5,51 +5,63 @@ from django.contrib.contenttypes.models import ContentType from bookwyrm.models import Connector, SiteSettings, User from bookwyrm.settings import DOMAIN + def init_groups(): - groups = ['admin', 'moderator', 'editor'] + groups = ["admin", "moderator", "editor"] for group in groups: Group.objects.create(name=group) + def init_permissions(): - permissions = [{ - 'codename': 'edit_instance_settings', - 'name': 'change the instance info', - 'groups': ['admin',] - }, { - 'codename': 'set_user_group', - 'name': 'change what group a user is in', - 'groups': ['admin', 'moderator'] - }, { - 'codename': 'control_federation', - 'name': 'control who to federate with', - 'groups': ['admin', 'moderator'] - }, { - 'codename': 'create_invites', - 'name': 'issue invitations to join', - 'groups': ['admin', 'moderator'] - }, { - 'codename': 'moderate_user', - 'name': 'deactivate or silence a user', - 'groups': ['admin', 'moderator'] - }, { - 'codename': 'moderate_post', - 'name': 'delete other users\' posts', - 'groups': ['admin', 'moderator'] - }, { - 'codename': 'edit_book', - 'name': 'edit book info', - 'groups': ['admin', 'moderator', 'editor'] - }] + permissions = [ + { + "codename": "edit_instance_settings", + "name": "change the instance info", + "groups": [ + "admin", + ], + }, + { + "codename": "set_user_group", + "name": "change what group a user is in", + "groups": ["admin", "moderator"], + }, + { + "codename": "control_federation", + "name": "control who to federate with", + "groups": ["admin", "moderator"], + }, + { + "codename": "create_invites", + "name": "issue invitations to join", + "groups": ["admin", "moderator"], + }, + { + "codename": "moderate_user", + "name": "deactivate or silence a user", + "groups": ["admin", "moderator"], + }, + { + "codename": "moderate_post", + "name": "delete other users' posts", + "groups": ["admin", "moderator"], + }, + { + "codename": "edit_book", + "name": "edit book info", + "groups": ["admin", "moderator", "editor"], + }, + ] content_type = ContentType.objects.get_for_model(User) for permission in permissions: permission_obj = Permission.objects.create( - codename=permission['codename'], - name=permission['name'], + codename=permission["codename"], + name=permission["name"], content_type=content_type, ) # add the permission to the appropriate groups - for group_name in permission['groups']: + for group_name in permission["groups"]: Group.objects.get(name=group_name).permissions.add(permission_obj) # while the groups and permissions shouldn't be changed because the code @@ -59,46 +71,48 @@ def init_permissions(): def init_connectors(): Connector.objects.create( identifier=DOMAIN, - name='Local', + name="Local", local=True, - connector_file='self_connector', - base_url='https://%s' % DOMAIN, - books_url='https://%s/book' % DOMAIN, - covers_url='https://%s/images/covers' % DOMAIN, - search_url='https://%s/search?q=' % DOMAIN, - isbn_search_url='https://%s/isbn/' % DOMAIN, + connector_file="self_connector", + base_url="https://%s" % DOMAIN, + books_url="https://%s/book" % DOMAIN, + covers_url="https://%s/images/covers" % DOMAIN, + search_url="https://%s/search?q=" % DOMAIN, + isbn_search_url="https://%s/isbn/" % DOMAIN, priority=1, ) Connector.objects.create( - identifier='bookwyrm.social', - name='BookWyrm dot Social', - connector_file='bookwyrm_connector', - base_url='https://bookwyrm.social', - books_url='https://bookwyrm.social/book', - covers_url='https://bookwyrm.social/images/covers', - search_url='https://bookwyrm.social/search?q=', - isbn_search_url='https://bookwyrm.social/isbn/', + identifier="bookwyrm.social", + name="BookWyrm dot Social", + connector_file="bookwyrm_connector", + base_url="https://bookwyrm.social", + books_url="https://bookwyrm.social/book", + covers_url="https://bookwyrm.social/images/covers", + search_url="https://bookwyrm.social/search?q=", + isbn_search_url="https://bookwyrm.social/isbn/", priority=2, ) Connector.objects.create( - identifier='openlibrary.org', - name='OpenLibrary', - connector_file='openlibrary', - base_url='https://openlibrary.org', - books_url='https://openlibrary.org', - covers_url='https://covers.openlibrary.org', - search_url='https://openlibrary.org/search?q=', - isbn_search_url='https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:', + identifier="openlibrary.org", + name="OpenLibrary", + connector_file="openlibrary", + base_url="https://openlibrary.org", + books_url="https://openlibrary.org", + covers_url="https://covers.openlibrary.org", + search_url="https://openlibrary.org/search?q=", + isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:", priority=3, ) + def init_settings(): SiteSettings.objects.create() + class Command(BaseCommand): - help = 'Initializes the database with starter data' + help = "Initializes the database with starter data" def handle(self, *args, **options): init_groups() diff --git a/bookwyrm/management/commands/remove_editions.py b/bookwyrm/management/commands/remove_editions.py index c5153f44b..6829c6d10 100644 --- a/bookwyrm/management/commands/remove_editions.py +++ b/bookwyrm/management/commands/remove_editions.py @@ -1,34 +1,42 @@ -''' PROCEED WITH CAUTION: this permanently deletes book data ''' +""" PROCEED WITH CAUTION: this permanently deletes book data """ from django.core.management.base import BaseCommand from django.db.models import Count, Q from bookwyrm import models def remove_editions(): - ''' combine duplicate editions and update related models ''' + """ combine duplicate editions and update related models """ # not in use - filters = {'%s__isnull' % r.name: True \ - for r in models.Edition._meta.related_objects} + filters = { + "%s__isnull" % r.name: True for r in models.Edition._meta.related_objects + } # no cover, no identifying fields - filters['cover'] = '' - null_fields = {'%s__isnull' % f: True for f in \ - ['isbn_10', 'isbn_13', 'oclc_number']} + filters["cover"] = "" + null_fields = { + "%s__isnull" % f: True for f in ["isbn_10", "isbn_13", "oclc_number"] + } - editions = models.Edition.objects.filter( - Q(languages=[]) | Q(languages__contains=['English']), - **filters, **null_fields - ).annotate(Count('parent_work__editions')).filter( - # mustn't be the only edition for the work - parent_work__editions__count__gt=1 + editions = ( + models.Edition.objects.filter( + Q(languages=[]) | Q(languages__contains=["English"]), + **filters, + **null_fields + ) + .annotate(Count("parent_work__editions")) + .filter( + # mustn't be the only edition for the work + parent_work__editions__count__gt=1 + ) ) print(editions.count()) editions.delete() class Command(BaseCommand): - ''' dedplucate allllll the book data models ''' - help = 'merges duplicate book data' + """ dedplucate allllll the book data models """ + + help = "merges duplicate book data" # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): - ''' run deudplications ''' + """ run deudplications """ remove_editions() diff --git a/bookwyrm/migrations/0001_initial.py b/bookwyrm/migrations/0001_initial.py index 347057e1d..a405b956f 100644 --- a/bookwyrm/migrations/0001_initial.py +++ b/bookwyrm/migrations/0001_initial.py @@ -15,199 +15,448 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0011_update_proxy_permissions'), + ("auth", "0011_update_proxy_permissions"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('private_key', models.TextField(blank=True, null=True)), - ('public_key', models.TextField(blank=True, null=True)), - ('actor', models.CharField(max_length=255, unique=True)), - ('inbox', models.CharField(max_length=255, unique=True)), - ('shared_inbox', models.CharField(blank=True, max_length=255, null=True)), - ('outbox', models.CharField(max_length=255, unique=True)), - ('summary', models.TextField(blank=True, null=True)), - ('local', models.BooleanField(default=True)), - ('fedireads_user', models.BooleanField(default=True)), - ('localname', models.CharField(max_length=255, null=True, unique=True)), - ('name', models.CharField(blank=True, max_length=100, null=True)), - ('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("private_key", models.TextField(blank=True, null=True)), + ("public_key", models.TextField(blank=True, null=True)), + ("actor", models.CharField(max_length=255, unique=True)), + ("inbox", models.CharField(max_length=255, unique=True)), + ( + "shared_inbox", + models.CharField(blank=True, max_length=255, null=True), + ), + ("outbox", models.CharField(max_length=255, unique=True)), + ("summary", models.TextField(blank=True, null=True)), + ("local", models.BooleanField(default=True)), + ("fedireads_user", models.BooleanField(default=True)), + ("localname", models.CharField(max_length=255, null=True, unique=True)), + ("name", models.CharField(blank=True, max_length=100, null=True)), + ( + "avatar", + models.ImageField(blank=True, null=True, upload_to="avatars/"), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='Author', + name="Author", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('openlibrary_key', models.CharField(max_length=255)), - ('data', JSONField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("openlibrary_key", models.CharField(max_length=255)), + ("data", JSONField()), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='Book', + name="Book", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('openlibrary_key', models.CharField(max_length=255, unique=True)), - ('data', JSONField()), - ('cover', models.ImageField(blank=True, null=True, upload_to='covers/')), - ('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), - ('authors', models.ManyToManyField(to='bookwyrm.Author')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("openlibrary_key", models.CharField(max_length=255, unique=True)), + ("data", JSONField()), + ( + "cover", + models.ImageField(blank=True, null=True, upload_to="covers/"), + ), + ( + "added_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ("authors", models.ManyToManyField(to="bookwyrm.Author")), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='FederatedServer', + name="FederatedServer", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('server_name', models.CharField(max_length=255, unique=True)), - ('status', models.CharField(default='federated', max_length=255)), - ('application_type', models.CharField(max_length=255, null=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("server_name", models.CharField(max_length=255, unique=True)), + ("status", models.CharField(default="federated", max_length=255)), + ("application_type", models.CharField(max_length=255, null=True)), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='Shelf', + name="Shelf", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('name', models.CharField(max_length=100)), - ('identifier', models.CharField(max_length=100)), - ('editable', models.BooleanField(default=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("name", models.CharField(max_length=100)), + ("identifier", models.CharField(max_length=100)), + ("editable", models.BooleanField(default=True)), ], ), migrations.CreateModel( - name='Status', + name="Status", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('status_type', models.CharField(default='Note', max_length=255)), - ('activity_type', models.CharField(default='Note', max_length=255)), - ('local', models.BooleanField(default=True)), - ('privacy', models.CharField(default='public', max_length=255)), - ('sensitive', models.BooleanField(default=False)), - ('mention_books', models.ManyToManyField(related_name='mention_book', to='bookwyrm.Book')), - ('mention_users', models.ManyToManyField(related_name='mention_user', to=settings.AUTH_USER_MODEL)), - ('reply_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, 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", + ), + ), + ("content", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("status_type", models.CharField(default="Note", max_length=255)), + ("activity_type", models.CharField(default="Note", max_length=255)), + ("local", models.BooleanField(default=True)), + ("privacy", models.CharField(default="public", max_length=255)), + ("sensitive", models.BooleanField(default=False)), + ( + "mention_books", + models.ManyToManyField( + related_name="mention_book", to="bookwyrm.Book" + ), + ), + ( + "mention_users", + models.ManyToManyField( + related_name="mention_user", to=settings.AUTH_USER_MODEL + ), + ), + ( + "reply_parent", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Status", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='UserRelationship', + name="UserRelationship", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('status', models.CharField(default='follows', max_length=100, null=True)), - ('relationship_id', models.CharField(max_length=100)), - ('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_object', to=settings.AUTH_USER_MODEL)), - ('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_subject', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.CharField(default="follows", max_length=100, null=True), + ), + ("relationship_id", models.CharField(max_length=100)), + ( + "user_object", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="user_object", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_subject", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="user_subject", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='ShelfBook', + name="ShelfBook", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), - ('shelf', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Shelf')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ( + "added_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book" + ), + ), + ( + "shelf", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Shelf" + ), + ), ], options={ - 'unique_together': {('book', 'shelf')}, + "unique_together": {("book", "shelf")}, }, ), migrations.AddField( - model_name='shelf', - name='books', - field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Book'), + model_name="shelf", + name="books", + field=models.ManyToManyField( + through="bookwyrm.ShelfBook", to="bookwyrm.Book" + ), ), migrations.AddField( - model_name='shelf', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="shelf", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='book', - name='shelves', - field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Shelf'), + model_name="book", + name="shelves", + field=models.ManyToManyField( + through="bookwyrm.ShelfBook", to="bookwyrm.Shelf" + ), ), migrations.AddField( - model_name='user', - name='federated_server', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.FederatedServer'), + model_name="user", + name="federated_server", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.FederatedServer", + ), ), migrations.AddField( - model_name='user', - name='followers', - field=models.ManyToManyField(through='bookwyrm.UserRelationship', to=settings.AUTH_USER_MODEL), + model_name="user", + name="followers", + field=models.ManyToManyField( + through="bookwyrm.UserRelationship", to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='user', - name='groups', - field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), + model_name="user", + name="groups", + field=models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), ), migrations.AddField( - model_name='user', - name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), + model_name="user", + name="user_permissions", + field=models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), ), migrations.AlterUniqueTogether( - name='shelf', - unique_together={('user', 'identifier')}, + name="shelf", + unique_together={("user", "identifier")}, ), migrations.CreateModel( - name='Review', + name="Review", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), - ('name', models.CharField(max_length=255)), - ('rating', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "rating", + models.IntegerField( + default=0, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book" + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), ] diff --git a/bookwyrm/migrations/0002_auto_20200219_0816.py b/bookwyrm/migrations/0002_auto_20200219_0816.py index 9cb5b726d..07daad935 100644 --- a/bookwyrm/migrations/0002_auto_20200219_0816.py +++ b/bookwyrm/migrations/0002_auto_20200219_0816.py @@ -8,31 +8,59 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0001_initial'), + ("bookwyrm", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Favorite', + name="Favorite", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, 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", + ), + ), + ("content", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Status", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'unique_together': {('user', 'status')}, + "unique_together": {("user", "status")}, }, ), migrations.AddField( - model_name='status', - name='favorites', - field=models.ManyToManyField(related_name='user_favorites', through='bookwyrm.Favorite', to=settings.AUTH_USER_MODEL), + model_name="status", + name="favorites", + field=models.ManyToManyField( + related_name="user_favorites", + through="bookwyrm.Favorite", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='user', - name='favorites', - field=models.ManyToManyField(related_name='favorite_statuses', through='bookwyrm.Favorite', to='bookwyrm.Status'), + model_name="user", + name="favorites", + field=models.ManyToManyField( + related_name="favorite_statuses", + through="bookwyrm.Favorite", + to="bookwyrm.Status", + ), ), ] diff --git a/bookwyrm/migrations/0003_auto_20200221_0131.py b/bookwyrm/migrations/0003_auto_20200221_0131.py index e53f042b4..e3e164140 100644 --- a/bookwyrm/migrations/0003_auto_20200221_0131.py +++ b/bookwyrm/migrations/0003_auto_20200221_0131.py @@ -7,87 +7,89 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0002_auto_20200219_0816'), + ("bookwyrm", "0002_auto_20200219_0816"), ] operations = [ migrations.RemoveField( - model_name='author', - name='content', + model_name="author", + name="content", ), migrations.RemoveField( - model_name='book', - name='content', + model_name="book", + name="content", ), migrations.RemoveField( - model_name='favorite', - name='content', + model_name="favorite", + name="content", ), migrations.RemoveField( - model_name='federatedserver', - name='content', + model_name="federatedserver", + name="content", ), migrations.RemoveField( - model_name='shelf', - name='content', + model_name="shelf", + name="content", ), migrations.RemoveField( - model_name='shelfbook', - name='content', + model_name="shelfbook", + name="content", ), migrations.RemoveField( - model_name='userrelationship', - name='content', + model_name="userrelationship", + name="content", ), migrations.AddField( - model_name='author', - name='updated_date', + model_name="author", + name="updated_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='book', - name='updated_date', + model_name="book", + name="updated_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='favorite', - name='updated_date', + model_name="favorite", + name="updated_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='federatedserver', - name='updated_date', + model_name="federatedserver", + name="updated_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='shelf', - name='updated_date', + model_name="shelf", + name="updated_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='shelfbook', - name='updated_date', + model_name="shelfbook", + name="updated_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='status', - name='updated_date', + model_name="status", + name="updated_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='user', - name='created_date', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + model_name="user", + name="created_date", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), preserve_default=False, ), migrations.AddField( - model_name='user', - name='updated_date', + model_name="user", + name="updated_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='userrelationship', - name='updated_date', + model_name="userrelationship", + name="updated_date", field=models.DateTimeField(auto_now=True), ), ] diff --git a/bookwyrm/migrations/0004_tag.py b/bookwyrm/migrations/0004_tag.py index 209550008..b6210070c 100644 --- a/bookwyrm/migrations/0004_tag.py +++ b/bookwyrm/migrations/0004_tag.py @@ -8,22 +8,41 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0003_auto_20200221_0131'), + ("bookwyrm", "0003_auto_20200221_0131"), ] operations = [ migrations.CreateModel( - name='Tag', + name="Tag", 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)), - ('name', models.CharField(max_length=140)), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), - ('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)), + ("name", models.CharField(max_length=140)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'unique_together': {('user', 'book', 'name')}, + "unique_together": {("user", "book", "name")}, }, ), ] diff --git a/bookwyrm/migrations/0005_auto_20200221_1645.py b/bookwyrm/migrations/0005_auto_20200221_1645.py index dbd87e924..449ce041e 100644 --- a/bookwyrm/migrations/0005_auto_20200221_1645.py +++ b/bookwyrm/migrations/0005_auto_20200221_1645.py @@ -5,27 +5,27 @@ from django.db import migrations, models def populate_identifiers(app_registry, schema_editor): db_alias = schema_editor.connection.alias - tags = app_registry.get_model('bookwyrm', 'Tag') + tags = app_registry.get_model("bookwyrm", "Tag") for tag in tags.objects.using(db_alias): - tag.identifier = re.sub(r'\W+', '-', tag.name).lower() + tag.identifier = re.sub(r"\W+", "-", tag.name).lower() tag.save() class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0004_tag'), + ("bookwyrm", "0004_tag"), ] operations = [ migrations.AddField( - model_name='tag', - name='identifier', + model_name="tag", + name="identifier", field=models.CharField(max_length=100, null=True), ), migrations.AlterField( - model_name='tag', - name='name', + model_name="tag", + name="name", field=models.CharField(max_length=100), ), migrations.RunPython(populate_identifiers), diff --git a/bookwyrm/migrations/0006_auto_20200221_1702_squashed_0064_merge_20201101_1913.py b/bookwyrm/migrations/0006_auto_20200221_1702_squashed_0064_merge_20201101_1913.py index 6a149ab59..c06fa40a0 100644 --- a/bookwyrm/migrations/0006_auto_20200221_1702_squashed_0064_merge_20201101_1913.py +++ b/bookwyrm/migrations/0006_auto_20200221_1702_squashed_0064_merge_20201101_1913.py @@ -16,1056 +16,1647 @@ import uuid class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0005_auto_20200221_1645'), + ("bookwyrm", "0005_auto_20200221_1645"), ] operations = [ migrations.AlterField( - model_name='tag', - name='identifier', + model_name="tag", + name="identifier", field=models.CharField(max_length=100), ), migrations.AddConstraint( - model_name='userrelationship', - constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='followers_unique'), + model_name="userrelationship", + constraint=models.UniqueConstraint( + fields=("user_subject", "user_object"), name="followers_unique" + ), ), migrations.RemoveField( - model_name='user', - name='followers', + model_name="user", + name="followers", ), migrations.AddField( - model_name='status', - name='published_date', + model_name="status", + name="published_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.CreateModel( - name='Edition', + name="Edition", fields=[ - ('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Book')), - ('isbn', models.CharField(max_length=255, null=True, unique=True)), - ('oclc_number', models.CharField(max_length=255, null=True, unique=True)), - ('pages', models.IntegerField(null=True)), + ( + "book_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Book", + ), + ), + ("isbn", models.CharField(max_length=255, null=True, unique=True)), + ( + "oclc_number", + models.CharField(max_length=255, null=True, unique=True), + ), + ("pages", models.IntegerField(null=True)), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.book',), + bases=("bookwyrm.book",), ), migrations.CreateModel( - name='Work', + name="Work", fields=[ - ('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Book')), - ('lccn', models.CharField(max_length=255, null=True, unique=True)), + ( + "book_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Book", + ), + ), + ("lccn", models.CharField(max_length=255, null=True, unique=True)), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.book',), + bases=("bookwyrm.book",), ), migrations.RemoveField( - model_name='author', - name='data', + model_name="author", + name="data", ), migrations.RemoveField( - model_name='book', - name='added_by', + model_name="book", + name="added_by", ), migrations.RemoveField( - model_name='book', - name='data', + model_name="book", + name="data", ), migrations.AddField( - model_name='author', - name='bio', + model_name="author", + name="bio", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='author', - name='born', + model_name="author", + name="born", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='author', - name='died', + model_name="author", + name="died", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='author', - name='first_name', + model_name="author", + name="first_name", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='author', - name='last_name', + model_name="author", + name="last_name", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='author', - name='name', - field=models.CharField(default='Unknown', max_length=255), + model_name="author", + name="name", + field=models.CharField(default="Unknown", max_length=255), preserve_default=False, ), migrations.AddField( - model_name='author', - name='wikipedia_link', + model_name="author", + name="wikipedia_link", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='description', + model_name="book", + name="description", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='book', - name='first_published_date', + model_name="book", + name="first_published_date", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='book', - name='language', + model_name="book", + name="language", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='last_sync_date', + model_name="book", + name="last_sync_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='book', - name='librarything_key', + model_name="book", + name="librarything_key", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.AddField( - model_name='book', - name='local_edits', + model_name="book", + name="local_edits", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='book', - name='local_key', + model_name="book", + name="local_key", field=models.CharField(default=uuid.uuid4, max_length=255, unique=True), ), migrations.AddField( - model_name='book', - name='misc_identifiers', + model_name="book", + name="misc_identifiers", field=JSONField(null=True), ), migrations.AddField( - model_name='book', - name='origin', + model_name="book", + name="origin", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.AddField( - model_name='book', - name='published_date', + model_name="book", + name="published_date", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='book', - name='series', + model_name="book", + name="series", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='series_number', + model_name="book", + name="series_number", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='sort_title', + model_name="book", + name="sort_title", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='subtitle', + model_name="book", + name="subtitle", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='book', - name='sync', + model_name="book", + name="sync", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='book', - name='title', - field=models.CharField(default='Unknown', max_length=255), + model_name="book", + name="title", + field=models.CharField(default="Unknown", max_length=255), preserve_default=False, ), migrations.AlterField( - model_name='author', - name='openlibrary_key', + model_name="author", + name="openlibrary_key", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.AlterField( - model_name='book', - name='openlibrary_key', + model_name="book", + name="openlibrary_key", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.AddField( - model_name='book', - name='parent_work', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Work'), + model_name="book", + name="parent_work", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Work", + ), ), migrations.CreateModel( - name='Notification', + name="Notification", 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)), - ('read', models.BooleanField(default=False)), - ('notification_type', models.CharField(max_length=255)), - ('related_book', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), - ('related_status', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status')), - ('related_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='related_user', to=settings.AUTH_USER_MODEL)), - ('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)), + ("read", models.BooleanField(default=False)), + ("notification_type", models.CharField(max_length=255)), + ( + "related_book", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Book", + ), + ), + ( + "related_status", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Status", + ), + ), + ( + "related_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="related_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.AddField( - model_name='author', - name='aliases', - field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="author", + name="aliases", + field=bookwyrm.models.fields.ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AddField( - model_name='user', - name='manually_approves_followers', + model_name="user", + name="manually_approves_followers", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='status', - name='remote_id', + model_name="status", + name="remote_id", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.CreateModel( - name='UserBlocks', + name="UserBlocks", 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)), - ('relationship_id', models.CharField(max_length=100)), - ('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_object', to=settings.AUTH_USER_MODEL)), - ('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_subject', 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)), + ("relationship_id", models.CharField(max_length=100)), + ( + "user_object", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userblocks_user_object", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_subject", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userblocks_user_subject", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='UserFollowRequest', + name="UserFollowRequest", 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)), - ('relationship_id', models.CharField(max_length=100)), - ('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_object', to=settings.AUTH_USER_MODEL)), - ('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_subject', 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)), + ("relationship_id", models.CharField(max_length=100)), + ( + "user_object", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollowrequest_user_object", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_subject", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollowrequest_user_subject", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='UserFollows', + name="UserFollows", 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)), - ('relationship_id', models.CharField(max_length=100)), - ('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_object', to=settings.AUTH_USER_MODEL)), - ('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_subject', 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)), + ("relationship_id", models.CharField(max_length=100)), + ( + "user_object", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollows_user_object", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_subject", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollows_user_subject", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.DeleteModel( - name='UserRelationship', + name="UserRelationship", ), migrations.AddField( - model_name='user', - name='blocks', - field=models.ManyToManyField(related_name='blocked_by', through='bookwyrm.UserBlocks', to=settings.AUTH_USER_MODEL), + model_name="user", + name="blocks", + field=models.ManyToManyField( + related_name="blocked_by", + through="bookwyrm.UserBlocks", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='user', - name='follow_requests', - field=models.ManyToManyField(related_name='follower_requests', through='bookwyrm.UserFollowRequest', to=settings.AUTH_USER_MODEL), + model_name="user", + name="follow_requests", + field=models.ManyToManyField( + related_name="follower_requests", + through="bookwyrm.UserFollowRequest", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='user', - name='following', - field=models.ManyToManyField(related_name='followers', through='bookwyrm.UserFollows', to=settings.AUTH_USER_MODEL), + model_name="user", + name="following", + field=models.ManyToManyField( + related_name="followers", + through="bookwyrm.UserFollows", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddConstraint( - model_name='userfollows', - constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userfollows_unique'), + model_name="userfollows", + constraint=models.UniqueConstraint( + fields=("user_subject", "user_object"), name="userfollows_unique" + ), ), migrations.AddConstraint( - model_name='userfollowrequest', - constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userfollowrequest_unique'), + model_name="userfollowrequest", + constraint=models.UniqueConstraint( + fields=("user_subject", "user_object"), name="userfollowrequest_unique" + ), ), migrations.AddConstraint( - model_name='userblocks', - constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userblocks_unique'), + model_name="userblocks", + constraint=models.UniqueConstraint( + fields=("user_subject", "user_object"), name="userblocks_unique" + ), ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + ] + ), + name="notification_type_valid", + ), ), migrations.AddConstraint( - model_name='userblocks', - constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userblocks_no_self'), + model_name="userblocks", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, + user_subject=django.db.models.expressions.F("user_object"), + ), + name="userblocks_no_self", + ), ), migrations.AddConstraint( - model_name='userfollowrequest', - constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userfollowrequest_no_self'), + model_name="userfollowrequest", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, + user_subject=django.db.models.expressions.F("user_object"), + ), + name="userfollowrequest_no_self", + ), ), migrations.AddConstraint( - model_name='userfollows', - constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userfollows_no_self'), + model_name="userfollows", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, + user_subject=django.db.models.expressions.F("user_object"), + ), + name="userfollows_no_self", + ), ), migrations.AddField( - model_name='favorite', - name='remote_id', + model_name="favorite", + name="remote_id", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.CreateModel( - name='Comment', + name="Comment", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), - ('name', models.CharField(max_length=255)), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book" + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), migrations.CreateModel( - name='Connector', + name="Connector", 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)), - ('identifier', models.CharField(max_length=255, unique=True)), - ('connector_file', models.CharField(choices=[('openlibrary', 'Openlibrary'), ('bookwyrm', 'BookWyrm')], default='openlibrary', max_length=255)), - ('is_self', models.BooleanField(default=False)), - ('api_key', models.CharField(max_length=255, null=True)), - ('base_url', models.CharField(max_length=255)), - ('covers_url', models.CharField(max_length=255)), - ('search_url', models.CharField(max_length=255, null=True)), - ('key_name', models.CharField(max_length=255)), - ('politeness_delay', models.IntegerField(null=True)), - ('max_query_count', models.IntegerField(null=True)), - ('query_count', models.IntegerField(default=0)), - ('query_count_expiry', models.DateTimeField(auto_now_add=True)), + ( + "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)), + ("identifier", models.CharField(max_length=255, unique=True)), + ( + "connector_file", + models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("bookwyrm", "BookWyrm"), + ], + default="openlibrary", + max_length=255, + ), + ), + ("is_self", models.BooleanField(default=False)), + ("api_key", models.CharField(max_length=255, null=True)), + ("base_url", models.CharField(max_length=255)), + ("covers_url", models.CharField(max_length=255)), + ("search_url", models.CharField(max_length=255, null=True)), + ("key_name", models.CharField(max_length=255)), + ("politeness_delay", models.IntegerField(null=True)), + ("max_query_count", models.IntegerField(null=True)), + ("query_count", models.IntegerField(default=0)), + ("query_count_expiry", models.DateTimeField(auto_now_add=True)), ], ), migrations.RenameField( - model_name='book', - old_name='local_key', - new_name='fedireads_key', + model_name="book", + old_name="local_key", + new_name="fedireads_key", ), migrations.RenameField( - model_name='book', - old_name='origin', - new_name='source_url', + model_name="book", + old_name="origin", + new_name="source_url", ), migrations.RemoveField( - model_name='book', - name='local_edits', + model_name="book", + name="local_edits", ), migrations.AddConstraint( - model_name='connector', - constraint=models.CheckConstraint(check=models.Q(connector_file__in=bookwyrm.models.connector.ConnectorFiles), name='connector_file_valid'), + model_name="connector", + constraint=models.CheckConstraint( + check=models.Q( + connector_file__in=bookwyrm.models.connector.ConnectorFiles + ), + name="connector_file_valid", + ), ), migrations.AddField( - model_name='book', - name='connector', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Connector'), + model_name="book", + name="connector", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Connector", + ), ), migrations.AddField( - model_name='book', - name='subject_places', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="book", + name="subject_places", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AddField( - model_name='book', - name='subjects', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="book", + name="subjects", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AddField( - model_name='edition', - name='publishers', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="edition", + name="publishers", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('fedireads_connector', 'Fedireads Connector')], default='openlibrary', max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("fedireads_connector", "Fedireads Connector"), + ], + default="openlibrary", + max_length=255, + ), ), migrations.RemoveField( - model_name='connector', - name='is_self', + model_name="connector", + name="is_self", ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('fedireads_connector', 'Fedireads Connector')], default='openlibrary', max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("self_connector", "Self Connector"), + ("fedireads_connector", "Fedireads Connector"), + ], + default="openlibrary", + max_length=255, + ), ), migrations.AddField( - model_name='book', - name='sync_cover', + model_name="book", + name="sync_cover", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='author', - name='born', + model_name="author", + name="born", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='author', - name='died', + model_name="author", + name="died", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='author', - name='fedireads_key', + model_name="author", + name="fedireads_key", field=models.CharField(default=uuid.uuid4, max_length=255, unique=True), ), migrations.AlterField( - model_name='author', - name='first_name', + model_name="author", + name="first_name", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='author', - name='last_name', + model_name="author", + name="last_name", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='author', - name='openlibrary_key', + model_name="author", + name="openlibrary_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='first_published_date', + model_name="book", + name="first_published_date", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='book', - name='goodreads_key', + model_name="book", + name="goodreads_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='language', + model_name="book", + name="language", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='librarything_key', + model_name="book", + name="librarything_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='openlibrary_key', + model_name="book", + name="openlibrary_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='published_date', + model_name="book", + name="published_date", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='book', - name='sort_title', + model_name="book", + name="sort_title", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='subtitle', + model_name="book", + name="subtitle", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='edition', - name='isbn', + model_name="edition", + name="isbn", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='edition', - name='oclc_number', + model_name="edition", + name="oclc_number", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='edition', - name='pages', + model_name="edition", + name="pages", field=models.IntegerField(blank=True, null=True), ), migrations.AddField( - model_name='edition', - name='physical_format', + model_name="edition", + name="physical_format", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='work', - name='lccn', + model_name="work", + name="lccn", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='federatedserver', - name='application_version', + model_name="federatedserver", + name="application_version", field=models.CharField(max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='last_sync_date', + model_name="book", + name="last_sync_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AlterField( - model_name='status', - name='published_date', + model_name="status", + name="published_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.CreateModel( - name='Boost', + name="Boost", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + ] + ), + name="notification_type_valid", + ), ), migrations.AddField( - model_name='boost', - name='boosted_status', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boosters', to='bookwyrm.Status'), + model_name="boost", + name="boosted_status", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="boosters", + to="bookwyrm.Status", + ), ), migrations.RemoveField( - model_name='book', - name='language', + model_name="book", + name="language", ), migrations.RemoveField( - model_name='book', - name='parent_work', + model_name="book", + name="parent_work", ), migrations.RemoveField( - model_name='book', - name='shelves', + model_name="book", + name="shelves", ), migrations.AddField( - model_name='book', - name='languages', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="book", + name="languages", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AddField( - model_name='edition', - name='default', + model_name="edition", + name="default", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='edition', - name='parent_work', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Work'), + model_name="edition", + name="parent_work", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Work", + ), ), migrations.AddField( - model_name='edition', - name='shelves', - field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Shelf'), + model_name="edition", + name="shelves", + field=models.ManyToManyField( + through="bookwyrm.ShelfBook", to="bookwyrm.Shelf" + ), ), migrations.AlterField( - model_name='comment', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="comment", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='notification', - name='related_book', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="notification", + name="related_book", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Edition", + ), ), migrations.AlterField( - model_name='review', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="review", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='shelf', - name='books', - field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Edition'), + model_name="shelf", + name="books", + field=models.ManyToManyField( + through="bookwyrm.ShelfBook", to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='shelfbook', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="shelfbook", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='status', - name='mention_books', - field=models.ManyToManyField(related_name='mention_book', to='bookwyrm.Edition'), + model_name="status", + name="mention_books", + field=models.ManyToManyField( + related_name="mention_book", to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='tag', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="tag", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.RemoveField( - model_name='comment', - name='name', + model_name="comment", + name="name", ), migrations.AlterField( - model_name='review', - name='rating', - field=models.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), + model_name="review", + name="rating", + field=models.IntegerField( + blank=True, + default=None, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(5), + ], + ), ), migrations.AlterField( - model_name='review', - name='name', + model_name="review", + name="name", field=models.CharField(max_length=255, null=True), ), migrations.CreateModel( - name='Quotation', + name="Quotation", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), - ('quote', models.TextField()), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), + ("quote", models.TextField()), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Edition", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), migrations.CreateModel( - name='ReadThrough', + name="ReadThrough", 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)), - ('pages_read', models.IntegerField(blank=True, null=True)), - ('start_date', models.DateTimeField(blank=True, null=True)), - ('finish_date', models.DateTimeField(blank=True, null=True)), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), - ('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)), + ("pages_read", models.IntegerField(blank=True, null=True)), + ("start_date", models.DateTimeField(blank=True, null=True)), + ("finish_date", models.DateTimeField(blank=True, null=True)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='ImportItem', + name="ImportItem", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('data', JSONField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("data", JSONField()), ], ), migrations.CreateModel( - name='ImportJob', + name="ImportJob", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(default=django.utils.timezone.now)), - ('task_id', models.CharField(max_length=100, null=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("task_id", models.CharField(max_length=100, null=True)), ], ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT_RESULT', 'Import Result')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT_RESULT", "Import Result"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT_RESULT']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT_RESULT", + ] + ), + name="notification_type_valid", + ), ), migrations.AddField( - model_name='importjob', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="importjob", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='importitem', - name='book', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='bookwyrm.Book'), + model_name="importitem", + name="book", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="bookwyrm.Book", + ), ), migrations.AddField( - model_name='importitem', - name='job', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='bookwyrm.ImportJob'), + model_name="importitem", + name="job", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="items", + to="bookwyrm.ImportJob", + ), ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AddField( - model_name='importitem', - name='fail_reason', + model_name="importitem", + name="fail_reason", field=models.TextField(null=True), ), migrations.AddField( - model_name='importitem', - name='index', + model_name="importitem", + name="index", field=models.IntegerField(default=1), preserve_default=False, ), migrations.AddField( - model_name='notification', - name='related_import', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.ImportJob'), + model_name="notification", + name="related_import", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.ImportJob", + ), ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT", + ] + ), + name="notification_type_valid", + ), ), migrations.RenameField( - model_name='edition', - old_name='isbn', - new_name='isbn_13', + model_name="edition", + old_name="isbn", + new_name="isbn_13", ), migrations.AddField( - model_name='book', - name='author_text', + model_name="book", + name="author_text", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='edition', - name='asin', + model_name="edition", + name="asin", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='edition', - name='isbn_10', + model_name="edition", + name="isbn_10", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='connector', - name='books_url', - field=models.CharField(default='https://openlibrary.org', max_length=255), + model_name="connector", + name="books_url", + field=models.CharField(default="https://openlibrary.org", max_length=255), preserve_default=False, ), migrations.AddField( - model_name='connector', - name='local', + model_name="connector", + name="local", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='connector', - name='name', + model_name="connector", + name="name", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='connector', - name='priority', + model_name="connector", + name="priority", field=models.IntegerField(default=2), ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('fedireads_connector', 'Fedireads Connector')], max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("self_connector", "Self Connector"), + ("fedireads_connector", "Fedireads Connector"), + ], + max_length=255, + ), ), migrations.RemoveField( - model_name='author', - name='fedireads_key', + model_name="author", + name="fedireads_key", ), migrations.RemoveField( - model_name='book', - name='fedireads_key', + model_name="book", + name="fedireads_key", ), migrations.RemoveField( - model_name='book', - name='source_url', + model_name="book", + name="source_url", ), migrations.AddField( - model_name='author', - name='last_sync_date', + model_name="author", + name="last_sync_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='author', - name='sync', + model_name="author", + name="sync", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='book', - name='remote_id', + model_name="book", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='author', - name='remote_id', + model_name="author", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.RemoveField( - model_name='book', - name='misc_identifiers', + model_name="book", + name="misc_identifiers", ), migrations.RemoveField( - model_name='connector', - name='key_name', + model_name="connector", + name="key_name", ), migrations.RemoveField( - model_name='user', - name='actor', + model_name="user", + name="actor", ), migrations.AddField( - model_name='connector', - name='remote_id', + model_name="connector", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='federatedserver', - name='remote_id', + model_name="federatedserver", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='notification', - name='remote_id', + model_name="notification", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='readthrough', - name='remote_id', + model_name="readthrough", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='shelf', - name='remote_id', + model_name="shelf", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='shelfbook', - name='remote_id', + model_name="shelfbook", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='tag', - name='remote_id', + model_name="tag", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='userblocks', - name='remote_id', + model_name="userblocks", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='userfollowrequest', - name='remote_id', + model_name="userfollowrequest", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='userfollows', - name='remote_id', + model_name="userfollows", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AlterField( - model_name='favorite', - name='remote_id', + model_name="favorite", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AlterField( - model_name='status', - name='remote_id', + model_name="status", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='user', - name='remote_id', + model_name="user", + name="remote_id", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.CreateModel( - name='SiteInvite', + name="SiteInvite", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(default=bookwyrm.models.site.new_access_code, max_length=32)), - ('expiry', models.DateTimeField(blank=True, null=True)), - ('use_limit', models.IntegerField(blank=True, null=True)), - ('times_used', models.IntegerField(default=0)), - ('user', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + default=bookwyrm.models.site.new_access_code, max_length=32 + ), + ), + ("expiry", models.DateTimeField(blank=True, null=True)), + ("use_limit", models.IntegerField(blank=True, null=True)), + ("times_used", models.IntegerField(default=0)), + ( + "user", + models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.RemoveField( - model_name='status', - name='activity_type', + model_name="status", + name="activity_type", ), migrations.RemoveField( - model_name='status', - name='status_type', + model_name="status", + name="status_type", ), migrations.RenameField( - model_name='user', - old_name='fedireads_user', - new_name='bookwyrm_user', + model_name="user", + old_name="fedireads_user", + new_name="bookwyrm_user", ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('bookwyrm_connector', 'BookWyrm Connector')], max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("self_connector", "Self Connector"), + ("bookwyrm_connector", "BookWyrm Connector"), + ], + max_length=255, + ), ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('bookwyrm_connector', 'Bookwyrm Connector')], max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("self_connector", "Self Connector"), + ("bookwyrm_connector", "Bookwyrm Connector"), + ], + max_length=255, + ), ), migrations.CreateModel( - name='GeneratedStatus', + name="GeneratedStatus", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), migrations.CreateModel( - name='PasswordReset', + name="PasswordReset", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(default=bookwyrm.models.site.new_access_code, max_length=32)), - ('expiry', models.DateTimeField(default=bookwyrm.models.site.get_passowrd_reset_expiry)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + default=bookwyrm.models.site.new_access_code, max_length=32 + ), + ), + ( + "expiry", + models.DateTimeField( + default=bookwyrm.models.site.get_passowrd_reset_expiry + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AlterField( - model_name='user', - name='email', + model_name="user", + name="email", field=models.EmailField(max_length=254, unique=True), ), migrations.CreateModel( - name='SiteSettings', + name="SiteSettings", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default='BookWyrm', max_length=100)), - ('instance_description', models.TextField(default='This instance has no description.')), - ('code_of_conduct', models.TextField(default='Add a code of conduct here.')), - ('allow_registration', models.BooleanField(default=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(default="BookWyrm", max_length=100)), + ( + "instance_description", + models.TextField(default="This instance has no description."), + ), + ( + "code_of_conduct", + models.TextField(default="Add a code of conduct here."), + ), + ("allow_registration", models.BooleanField(default=True)), ], ), migrations.AlterField( - model_name='user', - name='email', - field=models.EmailField(blank=True, max_length=254, verbose_name='email address'), + model_name="user", + name="email", + field=models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), ), migrations.AddField( - model_name='status', - name='deleted', + model_name="status", + name="deleted", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='status', - name='deleted_date', + model_name="status", + name="deleted_date", field=models.DateTimeField(), ), - django.contrib.postgres.operations.TrigramExtension( + django.contrib.postgres.operations.TrigramExtension(), + migrations.RemoveField( + model_name="userblocks", + name="relationship_id", ), migrations.RemoveField( - model_name='userblocks', - name='relationship_id', + model_name="userfollowrequest", + name="relationship_id", ), migrations.RemoveField( - model_name='userfollowrequest', - name='relationship_id', - ), - migrations.RemoveField( - model_name='userfollows', - name='relationship_id', + model_name="userfollows", + name="relationship_id", ), migrations.AlterField( - model_name='status', - name='deleted_date', + model_name="status", + name="deleted_date", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='status', - name='privacy', - field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="status", + name="privacy", + field=models.CharField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), migrations.AddField( - model_name='importjob', - name='include_reviews', + model_name="importjob", + name="include_reviews", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='importjob', - name='privacy', - field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="importjob", + name="privacy", + field=models.CharField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), migrations.AlterField( - model_name='user', - name='federated_server', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.FederatedServer'), + model_name="user", + name="federated_server", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.FederatedServer", + ), ), migrations.RenameModel( - old_name='GeneratedStatus', - new_name='GeneratedNote', + old_name="GeneratedStatus", + new_name="GeneratedNote", ), migrations.AlterField( - model_name='connector', - name='api_key', + model_name="connector", + name="api_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='connector', - name='max_query_count', + model_name="connector", + name="max_query_count", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='connector', - name='name', + model_name="connector", + name="name", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='connector', - name='politeness_delay', + model_name="connector", + name="politeness_delay", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='connector', - name='search_url', + model_name="connector", + name="search_url", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='user', - name='last_active_date', + model_name="user", + name="last_active_date", field=models.DateTimeField(auto_now=True), ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "MENTION", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT", + ] + ), + name="notification_type_valid", + ), ), ] diff --git a/bookwyrm/migrations/0007_auto_20201103_0014.py b/bookwyrm/migrations/0007_auto_20201103_0014.py index bf0a12eb0..116c97a3e 100644 --- a/bookwyrm/migrations/0007_auto_20201103_0014.py +++ b/bookwyrm/migrations/0007_auto_20201103_0014.py @@ -8,13 +8,15 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0006_auto_20200221_1702_squashed_0064_merge_20201101_1913'), + ("bookwyrm", "0006_auto_20200221_1702_squashed_0064_merge_20201101_1913"), ] operations = [ migrations.AlterField( - model_name='siteinvite', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="siteinvite", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), ] diff --git a/bookwyrm/migrations/0008_work_default_edition.py b/bookwyrm/migrations/0008_work_default_edition.py index da1f959e8..787e3776a 100644 --- a/bookwyrm/migrations/0008_work_default_edition.py +++ b/bookwyrm/migrations/0008_work_default_edition.py @@ -6,8 +6,8 @@ import django.db.models.deletion def set_default_edition(app_registry, schema_editor): db_alias = schema_editor.connection.alias - works = app_registry.get_model('bookwyrm', 'Work').objects.using(db_alias) - editions = app_registry.get_model('bookwyrm', 'Edition').objects.using(db_alias) + works = app_registry.get_model("bookwyrm", "Work").objects.using(db_alias) + editions = app_registry.get_model("bookwyrm", "Edition").objects.using(db_alias) for work in works: ed = editions.filter(parent_work=work, default=True).first() if not ed: @@ -15,21 +15,26 @@ def set_default_edition(app_registry, schema_editor): work.default_edition = ed work.save() + class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0007_auto_20201103_0014'), + ("bookwyrm", "0007_auto_20201103_0014"), ] operations = [ migrations.AddField( - model_name='work', - name='default_edition', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="work", + name="default_edition", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Edition", + ), ), migrations.RunPython(set_default_edition), migrations.RemoveField( - model_name='edition', - name='default', + model_name="edition", + name="default", ), ] diff --git a/bookwyrm/migrations/0009_shelf_privacy.py b/bookwyrm/migrations/0009_shelf_privacy.py index 8232c2edc..635661045 100644 --- a/bookwyrm/migrations/0009_shelf_privacy.py +++ b/bookwyrm/migrations/0009_shelf_privacy.py @@ -6,13 +6,22 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0008_work_default_edition'), + ("bookwyrm", "0008_work_default_edition"), ] operations = [ migrations.AddField( - model_name='shelf', - name='privacy', - field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="shelf", + name="privacy", + field=models.CharField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), ] diff --git a/bookwyrm/migrations/0010_importjob_retry.py b/bookwyrm/migrations/0010_importjob_retry.py index 21296cc45..b3cc371bb 100644 --- a/bookwyrm/migrations/0010_importjob_retry.py +++ b/bookwyrm/migrations/0010_importjob_retry.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0009_shelf_privacy'), + ("bookwyrm", "0009_shelf_privacy"), ] operations = [ migrations.AddField( - model_name='importjob', - name='retry', + model_name="importjob", + name="retry", field=models.BooleanField(default=False), ), ] diff --git a/bookwyrm/migrations/0011_auto_20201113_1727.py b/bookwyrm/migrations/0011_auto_20201113_1727.py index 15e853a35..f4ea55c59 100644 --- a/bookwyrm/migrations/0011_auto_20201113_1727.py +++ b/bookwyrm/migrations/0011_auto_20201113_1727.py @@ -2,9 +2,10 @@ from django.db import migrations, models + def set_origin_id(app_registry, schema_editor): db_alias = schema_editor.connection.alias - books = app_registry.get_model('bookwyrm', 'Book').objects.using(db_alias) + books = app_registry.get_model("bookwyrm", "Book").objects.using(db_alias) for book in books: book.origin_id = book.remote_id # the remote_id will be set automatically @@ -15,18 +16,18 @@ def set_origin_id(app_registry, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0010_importjob_retry'), + ("bookwyrm", "0010_importjob_retry"), ] operations = [ migrations.AddField( - model_name='author', - name='origin_id', + model_name="author", + name="origin_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='origin_id', + model_name="book", + name="origin_id", field=models.CharField(max_length=255, null=True), ), migrations.RunPython(set_origin_id), diff --git a/bookwyrm/migrations/0012_attachment.py b/bookwyrm/migrations/0012_attachment.py index 495538517..5188b463b 100644 --- a/bookwyrm/migrations/0012_attachment.py +++ b/bookwyrm/migrations/0012_attachment.py @@ -7,23 +7,41 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0011_auto_20201113_1727'), + ("bookwyrm", "0011_auto_20201113_1727"), ] operations = [ migrations.CreateModel( - name='Attachment', + name="Attachment", 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', models.CharField(max_length=255, null=True)), - ('image', models.ImageField(blank=True, null=True, upload_to='status/')), - ('caption', models.TextField(blank=True, null=True)), - ('status', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status')), + ( + "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", models.CharField(max_length=255, null=True)), + ( + "image", + models.ImageField(blank=True, null=True, upload_to="status/"), + ), + ("caption", models.TextField(blank=True, null=True)), + ( + "status", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="attachments", + to="bookwyrm.Status", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/bookwyrm/migrations/0012_progressupdate.py b/bookwyrm/migrations/0012_progressupdate.py index 131419712..566556b7e 100644 --- a/bookwyrm/migrations/0012_progressupdate.py +++ b/bookwyrm/migrations/0012_progressupdate.py @@ -8,24 +8,51 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0011_auto_20201113_1727'), + ("bookwyrm", "0011_auto_20201113_1727"), ] operations = [ migrations.CreateModel( - name='ProgressUpdate', + name="ProgressUpdate", 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', models.CharField(max_length=255, null=True)), - ('progress', models.IntegerField()), - ('mode', models.CharField(choices=[('PG', 'page'), ('PCT', 'percent')], default='PG', max_length=3)), - ('readthrough', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.ReadThrough')), - ('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", models.CharField(max_length=255, null=True)), + ("progress", models.IntegerField()), + ( + "mode", + models.CharField( + choices=[("PG", "page"), ("PCT", "percent")], + default="PG", + max_length=3, + ), + ), + ( + "readthrough", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.ReadThrough", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/bookwyrm/migrations/0013_book_origin_id.py b/bookwyrm/migrations/0013_book_origin_id.py index 581a2406e..08cf7bee7 100644 --- a/bookwyrm/migrations/0013_book_origin_id.py +++ b/bookwyrm/migrations/0013_book_origin_id.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0012_attachment'), + ("bookwyrm", "0012_attachment"), ] operations = [ migrations.AlterField( - model_name='book', - name='origin_id', + model_name="book", + name="origin_id", field=models.CharField(blank=True, max_length=255, null=True), ), ] diff --git a/bookwyrm/migrations/0014_auto_20201128_0118.py b/bookwyrm/migrations/0014_auto_20201128_0118.py index babdd7805..2626b9652 100644 --- a/bookwyrm/migrations/0014_auto_20201128_0118.py +++ b/bookwyrm/migrations/0014_auto_20201128_0118.py @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0013_book_origin_id'), + ("bookwyrm", "0013_book_origin_id"), ] operations = [ migrations.RenameModel( - old_name='Attachment', - new_name='Image', + old_name="Attachment", + new_name="Image", ), ] diff --git a/bookwyrm/migrations/0014_merge_20201128_0007.py b/bookwyrm/migrations/0014_merge_20201128_0007.py index e811fa7ff..ce6bb5c03 100644 --- a/bookwyrm/migrations/0014_merge_20201128_0007.py +++ b/bookwyrm/migrations/0014_merge_20201128_0007.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0013_book_origin_id'), - ('bookwyrm', '0012_progressupdate'), + ("bookwyrm", "0013_book_origin_id"), + ("bookwyrm", "0012_progressupdate"), ] - operations = [ - ] + operations = [] diff --git a/bookwyrm/migrations/0015_auto_20201128_0349.py b/bookwyrm/migrations/0015_auto_20201128_0349.py index 52b155186..f4454c5db 100644 --- a/bookwyrm/migrations/0015_auto_20201128_0349.py +++ b/bookwyrm/migrations/0015_auto_20201128_0349.py @@ -7,13 +7,18 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0014_auto_20201128_0118'), + ("bookwyrm", "0014_auto_20201128_0118"), ] operations = [ migrations.AlterField( - model_name='image', - name='status', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status'), + model_name="image", + name="status", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="attachments", + to="bookwyrm.Status", + ), ), ] diff --git a/bookwyrm/migrations/0015_auto_20201128_0734.py b/bookwyrm/migrations/0015_auto_20201128_0734.py index c6eb78150..efbad6109 100644 --- a/bookwyrm/migrations/0015_auto_20201128_0734.py +++ b/bookwyrm/migrations/0015_auto_20201128_0734.py @@ -6,18 +6,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0014_merge_20201128_0007'), + ("bookwyrm", "0014_merge_20201128_0007"), ] operations = [ migrations.RenameField( - model_name='readthrough', - old_name='pages_read', - new_name='progress', + model_name="readthrough", + old_name="pages_read", + new_name="progress", ), migrations.AddField( - model_name='readthrough', - name='progress_mode', - field=models.CharField(choices=[('PG', 'page'), ('PCT', 'percent')], default='PG', max_length=3), + model_name="readthrough", + name="progress_mode", + field=models.CharField( + choices=[("PG", "page"), ("PCT", "percent")], default="PG", max_length=3 + ), ), ] diff --git a/bookwyrm/migrations/0016_auto_20201129_0304.py b/bookwyrm/migrations/0016_auto_20201129_0304.py index 1e7159691..ef2cbe0f5 100644 --- a/bookwyrm/migrations/0016_auto_20201129_0304.py +++ b/bookwyrm/migrations/0016_auto_20201129_0304.py @@ -5,58 +5,101 @@ from django.db import migrations, models import django.db.models.deletion from django.contrib.postgres.fields import ArrayField + class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0015_auto_20201128_0349'), + ("bookwyrm", "0015_auto_20201128_0349"), ] operations = [ migrations.AlterField( - model_name='book', - name='subject_places', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None), + model_name="book", + name="subject_places", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + null=True, + size=None, + ), ), migrations.AlterField( - model_name='book', - name='subjects', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None), + model_name="book", + name="subjects", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + null=True, + size=None, + ), ), migrations.AlterField( - model_name='edition', - name='parent_work', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='editions', to='bookwyrm.Work'), + model_name="edition", + name="parent_work", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="editions", + to="bookwyrm.Work", + ), ), migrations.AlterField( - model_name='tag', - name='name', + model_name="tag", + name="name", field=models.CharField(max_length=100, unique=True), ), migrations.AlterUniqueTogether( - name='tag', + name="tag", unique_together=set(), ), migrations.RemoveField( - model_name='tag', - name='book', + model_name="tag", + name="book", ), migrations.RemoveField( - model_name='tag', - name='user', + model_name="tag", + name="user", ), migrations.CreateModel( - name='UserTag', + name="UserTag", 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', models.CharField(max_length=255, null=True)), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')), - ('tag', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Tag')), - ('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", models.CharField(max_length=255, null=True)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Edition", + ), + ), + ( + "tag", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Tag" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'unique_together': {('user', 'book', 'tag')}, + "unique_together": {("user", "book", "tag")}, }, ), ] diff --git a/bookwyrm/migrations/0016_auto_20201211_2026.py b/bookwyrm/migrations/0016_auto_20201211_2026.py index 46b6140c3..3793f90ba 100644 --- a/bookwyrm/migrations/0016_auto_20201211_2026.py +++ b/bookwyrm/migrations/0016_auto_20201211_2026.py @@ -6,23 +6,23 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0015_auto_20201128_0349'), + ("bookwyrm", "0015_auto_20201128_0349"), ] operations = [ migrations.AddField( - model_name='sitesettings', - name='admin_email', + model_name="sitesettings", + name="admin_email", field=models.EmailField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='sitesettings', - name='support_link', + model_name="sitesettings", + name="support_link", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='sitesettings', - name='support_title', + model_name="sitesettings", + name="support_title", field=models.CharField(blank=True, max_length=100, null=True), ), ] diff --git a/bookwyrm/migrations/0017_auto_20201130_1819.py b/bookwyrm/migrations/0017_auto_20201130_1819.py index 0775269b6..f6478e0a5 100644 --- a/bookwyrm/migrations/0017_auto_20201130_1819.py +++ b/bookwyrm/migrations/0017_auto_20201130_1819.py @@ -6,184 +6,296 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion + def copy_rsa_keys(app_registry, schema_editor): db_alias = schema_editor.connection.alias - users = app_registry.get_model('bookwyrm', 'User') - keypair = app_registry.get_model('bookwyrm', 'KeyPair') + users = app_registry.get_model("bookwyrm", "User") + keypair = app_registry.get_model("bookwyrm", "KeyPair") for user in users.objects.using(db_alias): if user.public_key or user.private_key: user.key_pair = keypair.objects.create( - remote_id='%s/#main-key' % user.remote_id, + remote_id="%s/#main-key" % user.remote_id, private_key=user.private_key, - public_key=user.public_key + public_key=user.public_key, ) user.save() + class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0016_auto_20201129_0304'), + ("bookwyrm", "0016_auto_20201129_0304"), ] operations = [ migrations.CreateModel( - name='KeyPair', + name="KeyPair", 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])), - ('private_key', models.TextField(blank=True, null=True)), - ('public_key', bookwyrm.models.fields.TextField(blank=True, null=True)), + ( + "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], + ), + ), + ("private_key", models.TextField(blank=True, null=True)), + ("public_key", bookwyrm.models.fields.TextField(blank=True, null=True)), ], options={ - 'abstract': False, + "abstract": False, }, bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model), ), migrations.AddField( - model_name='user', - name='followers', - field=bookwyrm.models.fields.ManyToManyField(related_name='following', through='bookwyrm.UserFollows', to=settings.AUTH_USER_MODEL), + model_name="user", + name="followers", + field=bookwyrm.models.fields.ManyToManyField( + related_name="following", + through="bookwyrm.UserFollows", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='author', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="author", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='book', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="book", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='connector', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="connector", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='favorite', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="favorite", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='federatedserver', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="federatedserver", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='image', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="image", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='notification', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="notification", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='readthrough', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="readthrough", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='shelf', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="shelf", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='shelfbook', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="shelfbook", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='status', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="status", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='tag', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="tag", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='user', - name='avatar', - field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='avatars/'), + model_name="user", + name="avatar", + field=bookwyrm.models.fields.ImageField( + blank=True, null=True, upload_to="avatars/" + ), ), migrations.AlterField( - model_name='user', - name='bookwyrm_user', + model_name="user", + name="bookwyrm_user", field=bookwyrm.models.fields.BooleanField(default=True), ), migrations.AlterField( - model_name='user', - name='inbox', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="user", + name="inbox", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + unique=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='user', - name='local', + model_name="user", + name="local", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='user', - name='manually_approves_followers', + model_name="user", + name="manually_approves_followers", field=bookwyrm.models.fields.BooleanField(default=False), ), migrations.AlterField( - model_name='user', - name='name', - field=bookwyrm.models.fields.CharField(blank=True, max_length=100, null=True), + model_name="user", + name="name", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=100, null=True + ), ), migrations.AlterField( - model_name='user', - name='outbox', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="user", + name="outbox", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + unique=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='user', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="user", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + unique=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='user', - name='shared_inbox', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="user", + name="shared_inbox", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='user', - name='summary', + model_name="user", + name="summary", field=bookwyrm.models.fields.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='user', - name='username', + model_name="user", + name="username", field=bookwyrm.models.fields.UsernameField(), ), migrations.AlterField( - model_name='userblocks', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="userblocks", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='userfollowrequest', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="userfollowrequest", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='userfollows', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="userfollows", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='usertag', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="usertag", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AddField( - model_name='user', - name='key_pair', - field=bookwyrm.models.fields.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to='bookwyrm.KeyPair'), + model_name="user", + name="key_pair", + field=bookwyrm.models.fields.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="owner", + to="bookwyrm.KeyPair", + ), ), migrations.RunPython(copy_rsa_keys), ] diff --git a/bookwyrm/migrations/0017_auto_20201212_0059.py b/bookwyrm/migrations/0017_auto_20201212_0059.py index c9e3fcf4e..34d27a1f9 100644 --- a/bookwyrm/migrations/0017_auto_20201212_0059.py +++ b/bookwyrm/migrations/0017_auto_20201212_0059.py @@ -7,13 +7,15 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0016_auto_20201211_2026'), + ("bookwyrm", "0016_auto_20201211_2026"), ] operations = [ migrations.AlterField( - model_name='readthrough', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="readthrough", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), ] diff --git a/bookwyrm/migrations/0018_auto_20201130_1832.py b/bookwyrm/migrations/0018_auto_20201130_1832.py index 278446cf5..579b09f2f 100644 --- a/bookwyrm/migrations/0018_auto_20201130_1832.py +++ b/bookwyrm/migrations/0018_auto_20201130_1832.py @@ -6,20 +6,20 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0017_auto_20201130_1819'), + ("bookwyrm", "0017_auto_20201130_1819"), ] operations = [ migrations.RemoveField( - model_name='user', - name='following', + model_name="user", + name="following", ), migrations.RemoveField( - model_name='user', - name='private_key', + model_name="user", + name="private_key", ), migrations.RemoveField( - model_name='user', - name='public_key', + model_name="user", + name="public_key", ), ] diff --git a/bookwyrm/migrations/0019_auto_20201130_1939.py b/bookwyrm/migrations/0019_auto_20201130_1939.py index 11cf6a3b6..e5e7674a1 100644 --- a/bookwyrm/migrations/0019_auto_20201130_1939.py +++ b/bookwyrm/migrations/0019_auto_20201130_1939.py @@ -3,34 +3,36 @@ import bookwyrm.models.fields from django.db import migrations + def update_notnull(app_registry, schema_editor): db_alias = schema_editor.connection.alias - users = app_registry.get_model('bookwyrm', 'User') + users = app_registry.get_model("bookwyrm", "User") for user in users.objects.using(db_alias): if user.name and user.summary: continue if not user.summary: - user.summary = '' + user.summary = "" if not user.name: - user.name = '' + user.name = "" user.save() + class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0018_auto_20201130_1832'), + ("bookwyrm", "0018_auto_20201130_1832"), ] operations = [ migrations.RunPython(update_notnull), migrations.AlterField( - model_name='user', - name='name', - field=bookwyrm.models.fields.CharField(default='', max_length=100), + model_name="user", + name="name", + field=bookwyrm.models.fields.CharField(default="", max_length=100), ), migrations.AlterField( - model_name='user', - name='summary', - field=bookwyrm.models.fields.TextField(default=''), + model_name="user", + name="summary", + field=bookwyrm.models.fields.TextField(default=""), ), ] diff --git a/bookwyrm/migrations/0020_auto_20201208_0213.py b/bookwyrm/migrations/0020_auto_20201208_0213.py index 9c5345c75..79d9e73dd 100644 --- a/bookwyrm/migrations/0020_auto_20201208_0213.py +++ b/bookwyrm/migrations/0020_auto_20201208_0213.py @@ -11,343 +11,497 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0019_auto_20201130_1939'), + ("bookwyrm", "0019_auto_20201130_1939"), ] operations = [ migrations.AlterField( - model_name='author', - name='aliases', - field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="author", + name="aliases", + field=bookwyrm.models.fields.ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AlterField( - model_name='author', - name='bio', + model_name="author", + name="bio", field=bookwyrm.models.fields.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='author', - name='born', + model_name="author", + name="born", field=bookwyrm.models.fields.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='author', - name='died', + model_name="author", + name="died", field=bookwyrm.models.fields.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='author', - name='name', + model_name="author", + name="name", field=bookwyrm.models.fields.CharField(max_length=255), ), migrations.AlterField( - model_name='author', - name='openlibrary_key', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="author", + name="openlibrary_key", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='author', - name='wikipedia_link', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="author", + name="wikipedia_link", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='book', - name='authors', - field=bookwyrm.models.fields.ManyToManyField(to='bookwyrm.Author'), + model_name="book", + name="authors", + field=bookwyrm.models.fields.ManyToManyField(to="bookwyrm.Author"), ), migrations.AlterField( - model_name='book', - name='cover', - field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='covers/'), + model_name="book", + name="cover", + field=bookwyrm.models.fields.ImageField( + blank=True, null=True, upload_to="covers/" + ), ), migrations.AlterField( - model_name='book', - name='description', + model_name="book", + name="description", field=bookwyrm.models.fields.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='book', - name='first_published_date', + model_name="book", + name="first_published_date", field=bookwyrm.models.fields.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='book', - name='goodreads_key', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="book", + name="goodreads_key", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='book', - name='languages', - field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="book", + name="languages", + field=bookwyrm.models.fields.ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AlterField( - model_name='book', - name='librarything_key', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="book", + name="librarything_key", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='book', - name='openlibrary_key', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="book", + name="openlibrary_key", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='book', - name='published_date', + model_name="book", + name="published_date", field=bookwyrm.models.fields.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='book', - name='series', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="book", + name="series", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='book', - name='series_number', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="book", + name="series_number", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='book', - name='sort_title', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="book", + name="sort_title", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='book', - name='subject_places', - field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None), + model_name="book", + name="subject_places", + field=bookwyrm.models.fields.ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + null=True, + size=None, + ), ), migrations.AlterField( - model_name='book', - name='subjects', - field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None), + model_name="book", + name="subjects", + field=bookwyrm.models.fields.ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + null=True, + size=None, + ), ), migrations.AlterField( - model_name='book', - name='subtitle', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="book", + name="subtitle", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='book', - name='title', + model_name="book", + name="title", field=bookwyrm.models.fields.CharField(max_length=255), ), migrations.AlterField( - model_name='boost', - name='boosted_status', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boosters', to='bookwyrm.Status'), + model_name="boost", + name="boosted_status", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="boosters", + to="bookwyrm.Status", + ), ), migrations.AlterField( - model_name='comment', - name='book', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="comment", + name="book", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='edition', - name='asin', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="edition", + name="asin", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='edition', - name='isbn_10', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="edition", + name="isbn_10", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='edition', - name='isbn_13', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="edition", + name="isbn_13", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='edition', - name='oclc_number', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="edition", + name="oclc_number", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='edition', - name='pages', + model_name="edition", + name="pages", field=bookwyrm.models.fields.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='edition', - name='parent_work', - field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='editions', to='bookwyrm.Work'), + model_name="edition", + name="parent_work", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="editions", + to="bookwyrm.Work", + ), ), migrations.AlterField( - model_name='edition', - name='physical_format', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="edition", + name="physical_format", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AlterField( - model_name='edition', - name='publishers', - field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="edition", + name="publishers", + field=bookwyrm.models.fields.ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AlterField( - model_name='favorite', - name='status', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'), + model_name="favorite", + name="status", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Status" + ), ), migrations.AlterField( - model_name='favorite', - name='user', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="favorite", + name="user", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( - model_name='image', - name='caption', + model_name="image", + name="caption", field=bookwyrm.models.fields.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='image', - name='image', - field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='status/'), + model_name="image", + name="image", + field=bookwyrm.models.fields.ImageField( + blank=True, null=True, upload_to="status/" + ), ), migrations.AlterField( - model_name='quotation', - name='book', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="quotation", + name="book", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='quotation', - name='quote', + model_name="quotation", + name="quote", field=bookwyrm.models.fields.TextField(), ), migrations.AlterField( - model_name='review', - name='book', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="review", + name="book", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='review', - name='name', + model_name="review", + name="name", field=bookwyrm.models.fields.CharField(max_length=255, null=True), ), migrations.AlterField( - model_name='review', - name='rating', - field=bookwyrm.models.fields.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), + model_name="review", + name="rating", + field=bookwyrm.models.fields.IntegerField( + blank=True, + default=None, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(5), + ], + ), ), migrations.AlterField( - model_name='shelf', - name='name', + model_name="shelf", + name="name", field=bookwyrm.models.fields.CharField(max_length=100), ), migrations.AlterField( - model_name='shelf', - name='privacy', - field=bookwyrm.models.fields.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="shelf", + name="privacy", + field=bookwyrm.models.fields.CharField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), migrations.AlterField( - model_name='shelf', - name='user', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="shelf", + name="user", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( - model_name='shelfbook', - name='added_by', - field=bookwyrm.models.fields.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="shelfbook", + name="added_by", + field=bookwyrm.models.fields.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='shelfbook', - name='book', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="shelfbook", + name="book", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='shelfbook', - name='shelf', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Shelf'), + model_name="shelfbook", + name="shelf", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Shelf" + ), ), migrations.AlterField( - model_name='status', - name='content', + model_name="status", + name="content", field=bookwyrm.models.fields.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='status', - name='mention_books', - field=bookwyrm.models.fields.TagField(related_name='mention_book', to='bookwyrm.Edition'), + model_name="status", + name="mention_books", + field=bookwyrm.models.fields.TagField( + related_name="mention_book", to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='status', - name='mention_users', - field=bookwyrm.models.fields.TagField(related_name='mention_user', to=settings.AUTH_USER_MODEL), + model_name="status", + name="mention_users", + field=bookwyrm.models.fields.TagField( + related_name="mention_user", to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( - model_name='status', - name='published_date', - field=bookwyrm.models.fields.DateTimeField(default=django.utils.timezone.now), + model_name="status", + name="published_date", + field=bookwyrm.models.fields.DateTimeField( + default=django.utils.timezone.now + ), ), migrations.AlterField( - model_name='status', - name='reply_parent', - field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'), + model_name="status", + name="reply_parent", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Status", + ), ), migrations.AlterField( - model_name='status', - name='sensitive', + model_name="status", + name="sensitive", field=bookwyrm.models.fields.BooleanField(default=False), ), migrations.AlterField( - model_name='status', - name='user', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="status", + name="user", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( - model_name='tag', - name='name', + model_name="tag", + name="name", field=bookwyrm.models.fields.CharField(max_length=100, unique=True), ), migrations.AlterField( - model_name='userblocks', - name='user_object', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_object', to=settings.AUTH_USER_MODEL), + model_name="userblocks", + name="user_object", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userblocks_user_object", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='userblocks', - name='user_subject', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_subject', to=settings.AUTH_USER_MODEL), + model_name="userblocks", + name="user_subject", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userblocks_user_subject", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='userfollowrequest', - name='user_object', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_object', to=settings.AUTH_USER_MODEL), + model_name="userfollowrequest", + name="user_object", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollowrequest_user_object", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='userfollowrequest', - name='user_subject', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_subject', to=settings.AUTH_USER_MODEL), + model_name="userfollowrequest", + name="user_subject", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollowrequest_user_subject", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='userfollows', - name='user_object', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_object', to=settings.AUTH_USER_MODEL), + model_name="userfollows", + name="user_object", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollows_user_object", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='userfollows', - name='user_subject', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_subject', to=settings.AUTH_USER_MODEL), + model_name="userfollows", + name="user_subject", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollows_user_subject", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='usertag', - name='book', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="usertag", + name="book", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='usertag', - name='tag', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Tag'), + model_name="usertag", + name="tag", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Tag" + ), ), migrations.AlterField( - model_name='usertag', - name='user', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="usertag", + name="user", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( - model_name='work', - name='default_edition', - field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="work", + name="default_edition", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Edition", + ), ), migrations.AlterField( - model_name='work', - name='lccn', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="work", + name="lccn", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), ] diff --git a/bookwyrm/migrations/0021_merge_20201212_1737.py b/bookwyrm/migrations/0021_merge_20201212_1737.py index 4ccf8c8cc..c6b48820d 100644 --- a/bookwyrm/migrations/0021_merge_20201212_1737.py +++ b/bookwyrm/migrations/0021_merge_20201212_1737.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0020_auto_20201208_0213'), - ('bookwyrm', '0016_auto_20201211_2026'), + ("bookwyrm", "0020_auto_20201208_0213"), + ("bookwyrm", "0016_auto_20201211_2026"), ] - operations = [ - ] + operations = [] diff --git a/bookwyrm/migrations/0022_auto_20201212_1744.py b/bookwyrm/migrations/0022_auto_20201212_1744.py index 0a98597f8..2651578cc 100644 --- a/bookwyrm/migrations/0022_auto_20201212_1744.py +++ b/bookwyrm/migrations/0022_auto_20201212_1744.py @@ -5,26 +5,27 @@ from django.db import migrations def set_author_name(app_registry, schema_editor): db_alias = schema_editor.connection.alias - authors = app_registry.get_model('bookwyrm', 'Author') + authors = app_registry.get_model("bookwyrm", "Author") for author in authors.objects.using(db_alias): if not author.name: - author.name = '%s %s' % (author.first_name, author.last_name) + author.name = "%s %s" % (author.first_name, author.last_name) author.save() + class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0021_merge_20201212_1737'), + ("bookwyrm", "0021_merge_20201212_1737"), ] operations = [ migrations.RunPython(set_author_name), migrations.RemoveField( - model_name='author', - name='first_name', + model_name="author", + name="first_name", ), migrations.RemoveField( - model_name='author', - name='last_name', + model_name="author", + name="last_name", ), ] diff --git a/bookwyrm/migrations/0023_auto_20201214_0511.py b/bookwyrm/migrations/0023_auto_20201214_0511.py index e811bded8..4b4a0c4a2 100644 --- a/bookwyrm/migrations/0023_auto_20201214_0511.py +++ b/bookwyrm/migrations/0023_auto_20201214_0511.py @@ -7,13 +7,22 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0022_auto_20201212_1744'), + ("bookwyrm", "0022_auto_20201212_1744"), ] operations = [ migrations.AlterField( - model_name='status', - name='privacy', - field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="status", + name="privacy", + field=bookwyrm.models.fields.PrivacyField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), ] diff --git a/bookwyrm/migrations/0023_merge_20201216_0112.py b/bookwyrm/migrations/0023_merge_20201216_0112.py index e3af48496..be88546e4 100644 --- a/bookwyrm/migrations/0023_merge_20201216_0112.py +++ b/bookwyrm/migrations/0023_merge_20201216_0112.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0017_auto_20201212_0059'), - ('bookwyrm', '0022_auto_20201212_1744'), + ("bookwyrm", "0017_auto_20201212_0059"), + ("bookwyrm", "0022_auto_20201212_1744"), ] - operations = [ - ] + operations = [] diff --git a/bookwyrm/migrations/0024_merge_20201216_1721.py b/bookwyrm/migrations/0024_merge_20201216_1721.py index 41f81335e..bb944d4eb 100644 --- a/bookwyrm/migrations/0024_merge_20201216_1721.py +++ b/bookwyrm/migrations/0024_merge_20201216_1721.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0023_auto_20201214_0511'), - ('bookwyrm', '0023_merge_20201216_0112'), + ("bookwyrm", "0023_auto_20201214_0511"), + ("bookwyrm", "0023_merge_20201216_0112"), ] - operations = [ - ] + operations = [] diff --git a/bookwyrm/migrations/0025_auto_20201217_0046.py b/bookwyrm/migrations/0025_auto_20201217_0046.py index a3ffe8c13..82e1f5037 100644 --- a/bookwyrm/migrations/0025_auto_20201217_0046.py +++ b/bookwyrm/migrations/0025_auto_20201217_0046.py @@ -7,33 +7,33 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0024_merge_20201216_1721'), + ("bookwyrm", "0024_merge_20201216_1721"), ] operations = [ migrations.AlterField( - model_name='author', - name='bio', + model_name="author", + name="bio", field=bookwyrm.models.fields.HtmlField(blank=True, null=True), ), migrations.AlterField( - model_name='book', - name='description', + model_name="book", + name="description", field=bookwyrm.models.fields.HtmlField(blank=True, null=True), ), migrations.AlterField( - model_name='quotation', - name='quote', + model_name="quotation", + name="quote", field=bookwyrm.models.fields.HtmlField(), ), migrations.AlterField( - model_name='status', - name='content', + model_name="status", + name="content", field=bookwyrm.models.fields.HtmlField(blank=True, null=True), ), migrations.AlterField( - model_name='user', - name='summary', - field=bookwyrm.models.fields.HtmlField(default=''), + model_name="user", + name="summary", + field=bookwyrm.models.fields.HtmlField(default=""), ), ] diff --git a/bookwyrm/migrations/0026_status_content_warning.py b/bookwyrm/migrations/0026_status_content_warning.py index f4e494db9..5212e83a0 100644 --- a/bookwyrm/migrations/0026_status_content_warning.py +++ b/bookwyrm/migrations/0026_status_content_warning.py @@ -7,13 +7,15 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0025_auto_20201217_0046'), + ("bookwyrm", "0025_auto_20201217_0046"), ] operations = [ migrations.AddField( - model_name='status', - name='content_warning', - field=bookwyrm.models.fields.CharField(blank=True, max_length=500, null=True), + model_name="status", + name="content_warning", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=500, null=True + ), ), ] diff --git a/bookwyrm/migrations/0027_auto_20201220_2007.py b/bookwyrm/migrations/0027_auto_20201220_2007.py index a3ad4dda3..5eec5139d 100644 --- a/bookwyrm/migrations/0027_auto_20201220_2007.py +++ b/bookwyrm/migrations/0027_auto_20201220_2007.py @@ -7,18 +7,20 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0026_status_content_warning'), + ("bookwyrm", "0026_status_content_warning"), ] operations = [ migrations.AlterField( - model_name='user', - name='name', - field=bookwyrm.models.fields.CharField(blank=True, max_length=100, null=True), + model_name="user", + name="name", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=100, null=True + ), ), migrations.AlterField( - model_name='user', - name='summary', + model_name="user", + name="summary", field=bookwyrm.models.fields.HtmlField(blank=True, null=True), ), ] diff --git a/bookwyrm/migrations/0028_remove_book_author_text.py b/bookwyrm/migrations/0028_remove_book_author_text.py index 8743c910d..1f91d1c1e 100644 --- a/bookwyrm/migrations/0028_remove_book_author_text.py +++ b/bookwyrm/migrations/0028_remove_book_author_text.py @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0027_auto_20201220_2007'), + ("bookwyrm", "0027_auto_20201220_2007"), ] operations = [ migrations.RemoveField( - model_name='book', - name='author_text', + model_name="book", + name="author_text", ), ] diff --git a/bookwyrm/migrations/0029_auto_20201221_2014.py b/bookwyrm/migrations/0029_auto_20201221_2014.py index ebf27a742..7a6b71801 100644 --- a/bookwyrm/migrations/0029_auto_20201221_2014.py +++ b/bookwyrm/migrations/0029_auto_20201221_2014.py @@ -9,53 +9,65 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0028_remove_book_author_text'), + ("bookwyrm", "0028_remove_book_author_text"), ] operations = [ migrations.RemoveField( - model_name='author', - name='last_sync_date', + model_name="author", + name="last_sync_date", ), migrations.RemoveField( - model_name='author', - name='sync', + model_name="author", + name="sync", ), migrations.RemoveField( - model_name='book', - name='last_sync_date', + model_name="book", + name="last_sync_date", ), migrations.RemoveField( - model_name='book', - name='sync', + model_name="book", + name="sync", ), migrations.RemoveField( - model_name='book', - name='sync_cover', + model_name="book", + name="sync_cover", ), migrations.AddField( - model_name='author', - name='goodreads_key', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="author", + name="goodreads_key", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AddField( - model_name='author', - name='last_edited_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="author", + name="last_edited_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='author', - name='librarything_key', - field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True), + model_name="author", + name="librarything_key", + field=bookwyrm.models.fields.CharField( + blank=True, max_length=255, null=True + ), ), migrations.AddField( - model_name='book', - name='last_edited_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="book", + name="last_edited_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='author', - name='origin_id', + model_name="author", + name="origin_id", field=models.CharField(blank=True, max_length=255, null=True), ), ] diff --git a/bookwyrm/migrations/0030_auto_20201224_1939.py b/bookwyrm/migrations/0030_auto_20201224_1939.py index 6de5d37fb..beee20c4b 100644 --- a/bookwyrm/migrations/0030_auto_20201224_1939.py +++ b/bookwyrm/migrations/0030_auto_20201224_1939.py @@ -7,13 +7,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0029_auto_20201221_2014'), + ("bookwyrm", "0029_auto_20201221_2014"), ] operations = [ migrations.AlterField( - model_name='user', - name='localname', - field=models.CharField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_localname]), + model_name="user", + name="localname", + field=models.CharField( + max_length=255, + null=True, + unique=True, + validators=[bookwyrm.models.fields.validate_localname], + ), ), ] diff --git a/bookwyrm/migrations/0031_auto_20210104_2040.py b/bookwyrm/migrations/0031_auto_20210104_2040.py index 604392d41..c6418fc9d 100644 --- a/bookwyrm/migrations/0031_auto_20210104_2040.py +++ b/bookwyrm/migrations/0031_auto_20210104_2040.py @@ -6,23 +6,23 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0030_auto_20201224_1939'), + ("bookwyrm", "0030_auto_20201224_1939"), ] operations = [ migrations.AddField( - model_name='sitesettings', - name='favicon', - field=models.ImageField(blank=True, null=True, upload_to='logos/'), + model_name="sitesettings", + name="favicon", + field=models.ImageField(blank=True, null=True, upload_to="logos/"), ), migrations.AddField( - model_name='sitesettings', - name='logo', - field=models.ImageField(blank=True, null=True, upload_to='logos/'), + model_name="sitesettings", + name="logo", + field=models.ImageField(blank=True, null=True, upload_to="logos/"), ), migrations.AddField( - model_name='sitesettings', - name='logo_small', - field=models.ImageField(blank=True, null=True, upload_to='logos/'), + model_name="sitesettings", + name="logo_small", + field=models.ImageField(blank=True, null=True, upload_to="logos/"), ), ] diff --git a/bookwyrm/migrations/0032_auto_20210104_2055.py b/bookwyrm/migrations/0032_auto_20210104_2055.py index 692cd581f..8b8012dab 100644 --- a/bookwyrm/migrations/0032_auto_20210104_2055.py +++ b/bookwyrm/migrations/0032_auto_20210104_2055.py @@ -6,18 +6,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0031_auto_20210104_2040'), + ("bookwyrm", "0031_auto_20210104_2040"), ] operations = [ migrations.AddField( - model_name='sitesettings', - name='instance_tagline', - field=models.CharField(default='Social Reading and Reviewing', max_length=150), + model_name="sitesettings", + name="instance_tagline", + field=models.CharField( + default="Social Reading and Reviewing", max_length=150 + ), ), migrations.AddField( - model_name='sitesettings', - name='registration_closed_text', - field=models.TextField(default='Contact an administrator to get an invite'), + model_name="sitesettings", + name="registration_closed_text", + field=models.TextField(default="Contact an administrator to get an invite"), ), ] diff --git a/bookwyrm/migrations/0033_siteinvite_created_date.py b/bookwyrm/migrations/0033_siteinvite_created_date.py index 9a3f98963..36d489ebb 100644 --- a/bookwyrm/migrations/0033_siteinvite_created_date.py +++ b/bookwyrm/migrations/0033_siteinvite_created_date.py @@ -7,14 +7,16 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0032_auto_20210104_2055'), + ("bookwyrm", "0032_auto_20210104_2055"), ] operations = [ migrations.AddField( - model_name='siteinvite', - name='created_date', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + model_name="siteinvite", + name="created_date", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), preserve_default=False, ), ] diff --git a/bookwyrm/migrations/0034_importjob_complete.py b/bookwyrm/migrations/0034_importjob_complete.py index 141706070..6593df9fd 100644 --- a/bookwyrm/migrations/0034_importjob_complete.py +++ b/bookwyrm/migrations/0034_importjob_complete.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0033_siteinvite_created_date'), + ("bookwyrm", "0033_siteinvite_created_date"), ] operations = [ migrations.AddField( - model_name='importjob', - name='complete', + model_name="importjob", + name="complete", field=models.BooleanField(default=False), ), ] diff --git a/bookwyrm/migrations/0035_edition_edition_rank.py b/bookwyrm/migrations/0035_edition_edition_rank.py index 1a75a0974..7465c31b4 100644 --- a/bookwyrm/migrations/0035_edition_edition_rank.py +++ b/bookwyrm/migrations/0035_edition_edition_rank.py @@ -6,20 +6,21 @@ from django.db import migrations def set_rank(app_registry, schema_editor): db_alias = schema_editor.connection.alias - books = app_registry.get_model('bookwyrm', 'Edition') + books = app_registry.get_model("bookwyrm", "Edition") for book in books.objects.using(db_alias): book.save() + class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0034_importjob_complete'), + ("bookwyrm", "0034_importjob_complete"), ] operations = [ migrations.AddField( - model_name='edition', - name='edition_rank', + model_name="edition", + name="edition_rank", field=bookwyrm.models.fields.IntegerField(default=0), ), migrations.RunPython(set_rank), diff --git a/bookwyrm/migrations/0036_annualgoal.py b/bookwyrm/migrations/0036_annualgoal.py index fb12833ea..fd08fb247 100644 --- a/bookwyrm/migrations/0036_annualgoal.py +++ b/bookwyrm/migrations/0036_annualgoal.py @@ -9,24 +9,57 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0035_edition_edition_rank'), + ("bookwyrm", "0035_edition_edition_rank"), ] operations = [ migrations.CreateModel( - name='AnnualGoal', + name="AnnualGoal", 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])), - ('goal', models.IntegerField()), - ('year', models.IntegerField(default=2021)), - ('privacy', models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)), - ('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], + ), + ), + ("goal", models.IntegerField()), + ("year", models.IntegerField(default=2021)), + ( + "privacy", + models.CharField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'unique_together': {('user', 'year')}, + "unique_together": {("user", "year")}, }, ), ] diff --git a/bookwyrm/migrations/0037_auto_20210118_1954.py b/bookwyrm/migrations/0037_auto_20210118_1954.py index 97ba8808a..a0c27d457 100644 --- a/bookwyrm/migrations/0037_auto_20210118_1954.py +++ b/bookwyrm/migrations/0037_auto_20210118_1954.py @@ -2,36 +2,39 @@ from django.db import migrations, models + def empty_to_null(apps, schema_editor): User = apps.get_model("bookwyrm", "User") db_alias = schema_editor.connection.alias User.objects.using(db_alias).filter(email="").update(email=None) + def null_to_empty(apps, schema_editor): User = apps.get_model("bookwyrm", "User") db_alias = schema_editor.connection.alias User.objects.using(db_alias).filter(email=None).update(email="") + class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0036_annualgoal'), + ("bookwyrm", "0036_annualgoal"), ] operations = [ migrations.AlterModelOptions( - name='shelfbook', - options={'ordering': ('-created_date',)}, + name="shelfbook", + options={"ordering": ("-created_date",)}, ), migrations.AlterField( - model_name='user', - name='email', + model_name="user", + name="email", field=models.EmailField(max_length=254, null=True), ), migrations.RunPython(empty_to_null, null_to_empty), migrations.AlterField( - model_name='user', - name='email', + model_name="user", + name="email", field=models.EmailField(max_length=254, null=True, unique=True), ), ] diff --git a/bookwyrm/migrations/0038_auto_20210119_1534.py b/bookwyrm/migrations/0038_auto_20210119_1534.py index ac7a0d68f..14fd1ff29 100644 --- a/bookwyrm/migrations/0038_auto_20210119_1534.py +++ b/bookwyrm/migrations/0038_auto_20210119_1534.py @@ -7,13 +7,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0037_auto_20210118_1954'), + ("bookwyrm", "0037_auto_20210118_1954"), ] operations = [ migrations.AlterField( - model_name='annualgoal', - name='goal', - field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)]), + model_name="annualgoal", + name="goal", + field=models.IntegerField( + validators=[django.core.validators.MinValueValidator(1)] + ), ), ] diff --git a/bookwyrm/migrations/0039_merge_20210120_0753.py b/bookwyrm/migrations/0039_merge_20210120_0753.py index 1af40ee93..e698d8eaf 100644 --- a/bookwyrm/migrations/0039_merge_20210120_0753.py +++ b/bookwyrm/migrations/0039_merge_20210120_0753.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0038_auto_20210119_1534'), - ('bookwyrm', '0015_auto_20201128_0734'), + ("bookwyrm", "0038_auto_20210119_1534"), + ("bookwyrm", "0015_auto_20201128_0734"), ] - operations = [ - ] + operations = [] diff --git a/bookwyrm/migrations/0040_auto_20210122_0057.py b/bookwyrm/migrations/0040_auto_20210122_0057.py index 8e528a899..0641f5273 100644 --- a/bookwyrm/migrations/0040_auto_20210122_0057.py +++ b/bookwyrm/migrations/0040_auto_20210122_0057.py @@ -9,28 +9,40 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0039_merge_20210120_0753'), + ("bookwyrm", "0039_merge_20210120_0753"), ] operations = [ migrations.AlterField( - model_name='progressupdate', - name='progress', - field=models.IntegerField(validators=[django.core.validators.MinValueValidator(0)]), + model_name="progressupdate", + name="progress", + field=models.IntegerField( + validators=[django.core.validators.MinValueValidator(0)] + ), ), migrations.AlterField( - model_name='progressupdate', - name='readthrough', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ReadThrough'), + model_name="progressupdate", + name="readthrough", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="bookwyrm.ReadThrough" + ), ), migrations.AlterField( - model_name='progressupdate', - name='remote_id', - field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]), + model_name="progressupdate", + name="remote_id", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), ), migrations.AlterField( - model_name='readthrough', - name='progress', - field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)]), + model_name="readthrough", + name="progress", + field=models.IntegerField( + blank=True, + null=True, + validators=[django.core.validators.MinValueValidator(0)], + ), ), ] diff --git a/bookwyrm/migrations/0041_auto_20210131_1614.py b/bookwyrm/migrations/0041_auto_20210131_1614.py index 6fcf406bd..01085dea3 100644 --- a/bookwyrm/migrations/0041_auto_20210131_1614.py +++ b/bookwyrm/migrations/0041_auto_20210131_1614.py @@ -10,56 +10,141 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0040_auto_20210122_0057'), + ("bookwyrm", "0040_auto_20210122_0057"), ] operations = [ migrations.CreateModel( - name='List', + name="List", 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])), - ('name', bookwyrm.models.fields.CharField(max_length=100)), - ('description', bookwyrm.models.fields.TextField(blank=True, null=True)), - ('privacy', bookwyrm.models.fields.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)), - ('curation', bookwyrm.models.fields.CharField(choices=[('closed', 'Closed'), ('open', 'Open'), ('curated', 'Curated')], default='closed', max_length=255)), + ( + "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], + ), + ), + ("name", bookwyrm.models.fields.CharField(max_length=100)), + ( + "description", + bookwyrm.models.fields.TextField(blank=True, null=True), + ), + ( + "privacy", + bookwyrm.models.fields.CharField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), + ), + ( + "curation", + bookwyrm.models.fields.CharField( + choices=[ + ("closed", "Closed"), + ("open", "Open"), + ("curated", "Curated"), + ], + default="closed", + max_length=255, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=(bookwyrm.models.activitypub_mixin.OrderedCollectionMixin, models.Model), + bases=( + bookwyrm.models.activitypub_mixin.OrderedCollectionMixin, + models.Model, + ), ), migrations.CreateModel( - name='ListItem', + name="ListItem", 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])), - ('notes', bookwyrm.models.fields.TextField(blank=True, null=True)), - ('approved', models.BooleanField(default=True)), - ('order', bookwyrm.models.fields.IntegerField(blank=True, null=True)), - ('added_by', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), - ('book', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')), - ('book_list', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.List')), - ('endorsement', models.ManyToManyField(related_name='endorsers', 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], + ), + ), + ("notes", bookwyrm.models.fields.TextField(blank=True, null=True)), + ("approved", models.BooleanField(default=True)), + ("order", bookwyrm.models.fields.IntegerField(blank=True, null=True)), + ( + "added_by", + bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "book", + bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Edition", + ), + ), + ( + "book_list", + bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="bookwyrm.List" + ), + ), + ( + "endorsement", + models.ManyToManyField( + related_name="endorsers", to=settings.AUTH_USER_MODEL + ), + ), ], options={ - 'ordering': ('-created_date',), - 'unique_together': {('book', 'book_list')}, + "ordering": ("-created_date",), + "unique_together": {("book", "book_list")}, }, bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model), ), migrations.AddField( - model_name='list', - name='books', - field=models.ManyToManyField(through='bookwyrm.ListItem', to='bookwyrm.Edition'), + model_name="list", + name="books", + field=models.ManyToManyField( + through="bookwyrm.ListItem", to="bookwyrm.Edition" + ), ), migrations.AddField( - model_name='list', - name='user', - field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="list", + name="user", + field=bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL + ), ), ] diff --git a/bookwyrm/migrations/0042_auto_20210201_2108.py b/bookwyrm/migrations/0042_auto_20210201_2108.py index 95a144de2..ee7201c10 100644 --- a/bookwyrm/migrations/0042_auto_20210201_2108.py +++ b/bookwyrm/migrations/0042_auto_20210201_2108.py @@ -7,22 +7,40 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0041_auto_20210131_1614'), + ("bookwyrm", "0041_auto_20210131_1614"), ] operations = [ migrations.AlterModelOptions( - name='list', - options={'ordering': ('-updated_date',)}, + name="list", + options={"ordering": ("-updated_date",)}, ), migrations.AlterField( - model_name='list', - name='privacy', - field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="list", + name="privacy", + field=bookwyrm.models.fields.PrivacyField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), migrations.AlterField( - model_name='shelf', - name='privacy', - field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="shelf", + name="privacy", + field=bookwyrm.models.fields.PrivacyField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), ] diff --git a/bookwyrm/migrations/0043_auto_20210204_2223.py b/bookwyrm/migrations/0043_auto_20210204_2223.py index b9c328eaf..2e8318c55 100644 --- a/bookwyrm/migrations/0043_auto_20210204_2223.py +++ b/bookwyrm/migrations/0043_auto_20210204_2223.py @@ -6,18 +6,18 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0042_auto_20210201_2108'), + ("bookwyrm", "0042_auto_20210201_2108"), ] operations = [ migrations.RenameField( - model_name='listitem', - old_name='added_by', - new_name='user', + model_name="listitem", + old_name="added_by", + new_name="user", ), migrations.RenameField( - model_name='shelfbook', - old_name='added_by', - new_name='user', + model_name="shelfbook", + old_name="added_by", + new_name="user", ), ] diff --git a/bookwyrm/migrations/0044_auto_20210207_1924.py b/bookwyrm/migrations/0044_auto_20210207_1924.py index 7289c73d8..897e8e025 100644 --- a/bookwyrm/migrations/0044_auto_20210207_1924.py +++ b/bookwyrm/migrations/0044_auto_20210207_1924.py @@ -5,9 +5,10 @@ from django.conf import settings from django.db import migrations import django.db.models.deletion + def set_user(app_registry, schema_editor): db_alias = schema_editor.connection.alias - shelfbook = app_registry.get_model('bookwyrm', 'ShelfBook') + shelfbook = app_registry.get_model("bookwyrm", "ShelfBook") for item in shelfbook.objects.using(db_alias).filter(user__isnull=True): item.user = item.shelf.user try: @@ -19,15 +20,19 @@ def set_user(app_registry, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0043_auto_20210204_2223'), + ("bookwyrm", "0043_auto_20210204_2223"), ] operations = [ migrations.RunPython(set_user, lambda x, y: None), migrations.AlterField( - model_name='shelfbook', - name='user', - field=bookwyrm.models.fields.ForeignKey(default=2, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + model_name="shelfbook", + name="user", + field=bookwyrm.models.fields.ForeignKey( + default=2, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), preserve_default=False, ), ] diff --git a/bookwyrm/migrations/0045_auto_20210210_2114.py b/bookwyrm/migrations/0045_auto_20210210_2114.py index 87b9a3188..22f33cf47 100644 --- a/bookwyrm/migrations/0045_auto_20210210_2114.py +++ b/bookwyrm/migrations/0045_auto_20210210_2114.py @@ -8,51 +8,102 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0044_auto_20210207_1924'), + ("bookwyrm", "0044_auto_20210207_1924"), ] operations = [ migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AddField( - model_name='notification', - name='related_list_item', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ListItem'), + model_name="notification", + name="related_list_item", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.ListItem", + ), ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import'), ('ADD', 'Add')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ("ADD", "Add"), + ], + max_length=255, + ), ), migrations.AlterField( - model_name='notification', - name='related_book', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.Edition'), + model_name="notification", + name="related_book", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.Edition", + ), ), migrations.AlterField( - model_name='notification', - name='related_import', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ImportJob'), + model_name="notification", + name="related_import", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.ImportJob", + ), ), migrations.AlterField( - model_name='notification', - name='related_status', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.Status'), + model_name="notification", + name="related_status", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.Status", + ), ), migrations.AlterField( - model_name='notification', - name='related_user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_user', to=settings.AUTH_USER_MODEL), + model_name="notification", + name="related_user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="related_user", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='notification', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="notification", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT', 'ADD']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "MENTION", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT", + "ADD", + ] + ), + name="notification_type_valid", + ), ), ] diff --git a/bookwyrm/migrations/0046_sitesettings_privacy_policy.py b/bookwyrm/migrations/0046_sitesettings_privacy_policy.py index 0c49d607c..f9193764c 100644 --- a/bookwyrm/migrations/0046_sitesettings_privacy_policy.py +++ b/bookwyrm/migrations/0046_sitesettings_privacy_policy.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0045_auto_20210210_2114'), + ("bookwyrm", "0045_auto_20210210_2114"), ] operations = [ migrations.AddField( - model_name='sitesettings', - name='privacy_policy', - field=models.TextField(default='Add a privacy policy here.'), + model_name="sitesettings", + name="privacy_policy", + field=models.TextField(default="Add a privacy policy here."), ), ] diff --git a/bookwyrm/migrations/0047_connector_isbn_search_url.py b/bookwyrm/migrations/0047_connector_isbn_search_url.py index 617a89d9d..2ca802c5a 100644 --- a/bookwyrm/migrations/0047_connector_isbn_search_url.py +++ b/bookwyrm/migrations/0047_connector_isbn_search_url.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0046_sitesettings_privacy_policy'), + ("bookwyrm", "0046_sitesettings_privacy_policy"), ] operations = [ migrations.AddField( - model_name='connector', - name='isbn_search_url', + model_name="connector", + name="isbn_search_url", field=models.CharField(blank=True, max_length=255, null=True), ), ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 0aef63850..bef9debea 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -1,4 +1,4 @@ -''' bring all the models into the app namespace ''' +""" bring all the models into the app namespace """ import inspect import sys @@ -27,8 +27,12 @@ from .import_job import ImportJob, ImportItem from .site import SiteSettings, SiteInvite, PasswordReset cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) -activity_models = {c[1].activity_serializer.__name__: c[1] \ - for c in cls_members if hasattr(c[1], 'activity_serializer')} +activity_models = { + c[1].activity_serializer.__name__: c[1] + for c in cls_members + if hasattr(c[1], "activity_serializer") +} status_models = [ - c.__name__ for (_, c) in activity_models.items() if issubclass(c, Status)] + c.__name__ for (_, c) in activity_models.items() if issubclass(c, Status) +] diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 10015bf14..4ced78c28 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -1,4 +1,4 @@ -''' activitypub model functionality ''' +""" activitypub model functionality """ from base64 import b64encode from functools import reduce import json @@ -26,18 +26,19 @@ logger = logging.getLogger(__name__) # I tried to separate these classes into mutliple files but I kept getting # circular import errors so I gave up. I'm sure it could be done though! class ActivitypubMixin: - ''' add this mixin for models that are AP serializable ''' + """ add this mixin for models that are AP serializable """ + activity_serializer = lambda: {} reverse_unfurl = False def __init__(self, *args, **kwargs): - ''' collect some info on model fields ''' + """ collect some info on model fields """ self.image_fields = [] self.many_to_many_fields = [] - self.simple_fields = [] # "simple" + self.simple_fields = [] # "simple" # sort model fields by type for field in self._meta.get_fields(): - if not hasattr(field, 'field_to_activity'): + if not hasattr(field, "field_to_activity"): continue if isinstance(field, ImageField): @@ -48,33 +49,41 @@ class ActivitypubMixin: self.simple_fields.append(field) # a list of allll the serializable fields - self.activity_fields = self.image_fields + \ - self.many_to_many_fields + self.simple_fields + self.activity_fields = ( + self.image_fields + self.many_to_many_fields + self.simple_fields + ) # these are separate to avoid infinite recursion issues - self.deserialize_reverse_fields = self.deserialize_reverse_fields \ - if hasattr(self, 'deserialize_reverse_fields') else [] - self.serialize_reverse_fields = self.serialize_reverse_fields \ - if hasattr(self, 'serialize_reverse_fields') else [] + self.deserialize_reverse_fields = ( + self.deserialize_reverse_fields + if hasattr(self, "deserialize_reverse_fields") + else [] + ) + self.serialize_reverse_fields = ( + self.serialize_reverse_fields + if hasattr(self, "serialize_reverse_fields") + else [] + ) super().__init__(*args, **kwargs) - @classmethod def find_existing_by_remote_id(cls, remote_id): - ''' look up a remote id in the db ''' - return cls.find_existing({'id': remote_id}) + """ look up a remote id in the db """ + return cls.find_existing({"id": remote_id}) @classmethod def find_existing(cls, data): - ''' compare data to fields that can be used for deduplation. + """compare data to fields that can be used for deduplation. This always includes remote_id, but can also be unique identifiers - like an isbn for an edition ''' + like an isbn for an edition""" filters = [] # grabs all the data from the model to create django queryset filters for field in cls._meta.get_fields(): - if not hasattr(field, 'deduplication_field') or \ - not field.deduplication_field: + if ( + not hasattr(field, "deduplication_field") + or not field.deduplication_field + ): continue value = data.get(field.get_activitypub_field()) @@ -82,9 +91,9 @@ class ActivitypubMixin: continue filters.append({field.name: value}) - if hasattr(cls, 'origin_id') and 'id' in data: + if hasattr(cls, "origin_id") and "id" in data: # kinda janky, but this handles special case for books - filters.append({'origin_id': data['id']}) + filters.append({"origin_id": data["id"]}) if not filters: # if there are no deduplication fields, it will match the first @@ -92,45 +101,41 @@ class ActivitypubMixin: return None objects = cls.objects - if hasattr(objects, 'select_subclasses'): + if hasattr(objects, "select_subclasses"): objects = objects.select_subclasses() # an OR operation on all the match fields, sorry for the dense syntax - match = objects.filter( - reduce(operator.or_, (Q(**f) for f in filters)) - ) + match = objects.filter(reduce(operator.or_, (Q(**f) for f in filters))) # there OUGHT to be only one match return match.first() - def broadcast(self, activity, sender, software=None): - ''' send out an activity ''' + """ send out an activity """ broadcast_task.delay( sender.id, json.dumps(activity, cls=activitypub.ActivityEncoder), - self.get_recipients(software=software) + self.get_recipients(software=software), ) - def get_recipients(self, software=None): - ''' figure out which inbox urls to post to ''' + """ figure out which inbox urls to post to """ # first we have to figure out who should receive this activity - privacy = self.privacy if hasattr(self, 'privacy') else 'public' + privacy = self.privacy if hasattr(self, "privacy") else "public" # is this activity owned by a user (statuses, lists, shelves), or is it # general to the instance (like books) - user = self.user if hasattr(self, 'user') else None - user_model = apps.get_model('bookwyrm.User', require_ready=True) + user = self.user if hasattr(self, "user") else None + user_model = apps.get_model("bookwyrm.User", require_ready=True) if not user and isinstance(self, user_model): # or maybe the thing itself is a user user = self # find anyone who's tagged in a status, for example - mentions = self.recipients if hasattr(self, 'recipients') else [] + mentions = self.recipients if hasattr(self, "recipients") else [] # we always send activities to explicitly mentioned users' inboxes recipients = [u.inbox for u in mentions or []] # unless it's a dm, all the followers should receive the activity - if privacy != 'direct': + if privacy != "direct": # we will send this out to a subset of all remote users queryset = user_model.objects.filter( local=False, @@ -138,43 +143,43 @@ class ActivitypubMixin: # filter users first by whether they're using the desired software # this lets us send book updates only to other bw servers if software: - queryset = queryset.filter( - bookwyrm_user=(software == 'bookwyrm') - ) + queryset = queryset.filter(bookwyrm_user=(software == "bookwyrm")) # if there's a user, we only want to send to the user's followers if user: queryset = queryset.filter(following=user) # ideally, we will send to shared inboxes for efficiency - shared_inboxes = queryset.filter( - shared_inbox__isnull=False - ).values_list('shared_inbox', flat=True).distinct() + shared_inboxes = ( + queryset.filter(shared_inbox__isnull=False) + .values_list("shared_inbox", flat=True) + .distinct() + ) # but not everyone has a shared inbox - inboxes = queryset.filter( - shared_inbox__isnull=True - ).values_list('inbox', flat=True) + inboxes = queryset.filter(shared_inbox__isnull=True).values_list( + "inbox", flat=True + ) recipients += list(shared_inboxes) + list(inboxes) return recipients - def to_activity_dataclass(self): - ''' convert from a model to an activity ''' + """ convert from a model to an activity """ activity = generate_activity(self) return self.activity_serializer(**activity) - def to_activity(self, **kwargs): # pylint: disable=unused-argument - ''' convert from a model to a json activity ''' + def to_activity(self, **kwargs): # pylint: disable=unused-argument + """ convert from a model to a json activity """ return self.to_activity_dataclass().serialize() class ObjectMixin(ActivitypubMixin): - ''' add this mixin for object models that are AP serializable ''' + """ add this mixin for object models that are AP serializable """ + def save(self, *args, created=None, **kwargs): - ''' broadcast created/updated/deleted objects as appropriate ''' - broadcast = kwargs.get('broadcast', True) + """ broadcast created/updated/deleted objects as appropriate """ + broadcast = kwargs.get("broadcast", True) # this bonus kwarg woul cause an error in the base save method - if 'broadcast' in kwargs: - del kwargs['broadcast'] + if "broadcast" in kwargs: + del kwargs["broadcast"] created = created or not bool(self.id) # first off, we want to save normally no matter what @@ -183,7 +188,7 @@ class ObjectMixin(ActivitypubMixin): return # this will work for objects owned by a user (lists, shelves) - user = self.user if hasattr(self, 'user') else None + user = self.user if hasattr(self, "user") else None if created: # broadcast Create activities for objects owned by a local user @@ -193,10 +198,10 @@ class ObjectMixin(ActivitypubMixin): try: software = None # do we have a "pure" activitypub version of this for mastodon? - if hasattr(self, 'pure_content'): + if hasattr(self, "pure_content"): pure_activity = self.to_create_activity(user, pure=True) - self.broadcast(pure_activity, user, software='other') - software = 'bookwyrm' + self.broadcast(pure_activity, user, software="other") + software = "bookwyrm" # sends to BW only if we just did a pure version for masto activity = self.to_create_activity(user) self.broadcast(activity, user, software=software) @@ -209,39 +214,38 @@ class ObjectMixin(ActivitypubMixin): # --- updating an existing object if not user: # users don't have associated users, they ARE users - user_model = apps.get_model('bookwyrm.User', require_ready=True) + user_model = apps.get_model("bookwyrm.User", require_ready=True) if isinstance(self, user_model): user = self # book data tracks last editor - elif hasattr(self, 'last_edited_by'): + elif hasattr(self, "last_edited_by"): user = self.last_edited_by # again, if we don't know the user or they're remote, don't bother if not user or not user.local: return # is this a deletion? - if hasattr(self, 'deleted') and self.deleted: + if hasattr(self, "deleted") and self.deleted: activity = self.to_delete_activity(user) else: activity = self.to_update_activity(user) self.broadcast(activity, user) - def to_create_activity(self, user, **kwargs): - ''' returns the object wrapped in a Create activity ''' + """ returns the object wrapped in a Create activity """ activity_object = self.to_activity_dataclass(**kwargs) signature = None - create_id = self.remote_id + '/activity' - if hasattr(activity_object, 'content') and activity_object.content: + create_id = self.remote_id + "/activity" + if hasattr(activity_object, "content") and activity_object.content: signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key)) content = activity_object.content - signed_message = signer.sign(SHA256.new(content.encode('utf8'))) + signed_message = signer.sign(SHA256.new(content.encode("utf8"))) signature = activitypub.Signature( - creator='%s#main-key' % user.remote_id, + creator="%s#main-key" % user.remote_id, created=activity_object.published, - signatureValue=b64encode(signed_message).decode('utf8') + signatureValue=b64encode(signed_message).decode("utf8"), ) return activitypub.Create( @@ -253,50 +257,48 @@ class ObjectMixin(ActivitypubMixin): signature=signature, ).serialize() - def to_delete_activity(self, user): - ''' notice of deletion ''' + """ notice of deletion """ return activitypub.Delete( - id=self.remote_id + '/activity', + id=self.remote_id + "/activity", actor=user.remote_id, - to=['%s/followers' % user.remote_id], - cc=['https://www.w3.org/ns/activitystreams#Public'], + to=["%s/followers" % user.remote_id], + cc=["https://www.w3.org/ns/activitystreams#Public"], + object=self, + ).serialize() + + def to_update_activity(self, user): + """ wrapper for Updates to an activity """ + activity_id = "%s#update/%s" % (self.remote_id, uuid4()) + return activitypub.Update( + id=activity_id, + actor=user.remote_id, + to=["https://www.w3.org/ns/activitystreams#Public"], object=self, ).serialize() - def to_update_activity(self, user): - ''' wrapper for Updates to an activity ''' - activity_id = '%s#update/%s' % (self.remote_id, uuid4()) - return activitypub.Update( - id=activity_id, - actor=user.remote_id, - to=['https://www.w3.org/ns/activitystreams#Public'], - object=self - ).serialize() - - class OrderedCollectionPageMixin(ObjectMixin): - ''' just the paginator utilities, so you don't HAVE to - override ActivitypubMixin's to_activity (ie, for outbox) ''' + """just the paginator utilities, so you don't HAVE to + override ActivitypubMixin's to_activity (ie, for outbox)""" + @property def collection_remote_id(self): - ''' this can be overriden if there's a special remote id, ie outbox ''' + """ this can be overriden if there's a special remote id, ie outbox """ return self.remote_id - - def to_ordered_collection(self, queryset, \ - remote_id=None, page=False, collection_only=False, **kwargs): - ''' an ordered collection of whatevers ''' + def to_ordered_collection( + self, queryset, remote_id=None, page=False, collection_only=False, **kwargs + ): + """ an ordered collection of whatevers """ if not queryset.ordered: - raise RuntimeError('queryset must be ordered') + raise RuntimeError("queryset must be ordered") remote_id = remote_id or self.remote_id if page: - return to_ordered_collection_page( - queryset, remote_id, **kwargs) + return to_ordered_collection_page(queryset, remote_id, **kwargs) - if collection_only or not hasattr(self, 'activity_serializer'): + if collection_only or not hasattr(self, "activity_serializer"): serializer = activitypub.OrderedCollection activity = {} else: @@ -305,23 +307,24 @@ class OrderedCollectionPageMixin(ObjectMixin): activity = generate_activity(self) if remote_id: - activity['id'] = remote_id + activity["id"] = remote_id paginated = Paginator(queryset, PAGE_LENGTH) # add computed fields specific to orderd collections - activity['totalItems'] = paginated.count - activity['first'] = '%s?page=1' % remote_id - activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages) + activity["totalItems"] = paginated.count + activity["first"] = "%s?page=1" % remote_id + activity["last"] = "%s?page=%d" % (remote_id, paginated.num_pages) return serializer(**activity) class OrderedCollectionMixin(OrderedCollectionPageMixin): - ''' extends activitypub models to work as ordered collections ''' + """ extends activitypub models to work as ordered collections """ + @property def collection_queryset(self): - ''' usually an ordered collection model aggregates a different model ''' - raise NotImplementedError('Model must define collection_queryset') + """ usually an ordered collection model aggregates a different model """ + raise NotImplementedError("Model must define collection_queryset") activity_serializer = activitypub.OrderedCollection @@ -329,18 +332,20 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin): return self.to_ordered_collection(self.collection_queryset, **kwargs) def to_activity(self, **kwargs): - ''' an ordered collection of the specified model queryset ''' + """ an ordered collection of the specified model queryset """ return self.to_ordered_collection( - self.collection_queryset, **kwargs).serialize() + self.collection_queryset, **kwargs + ).serialize() class CollectionItemMixin(ActivitypubMixin): - ''' for items that are part of an (Ordered)Collection ''' + """ for items that are part of an (Ordered)Collection """ + activity_serializer = activitypub.Add object_field = collection_field = None def save(self, *args, broadcast=True, **kwargs): - ''' broadcast updated ''' + """ broadcast updated """ created = not bool(self.id) # first off, we want to save normally no matter what super().save(*args, **kwargs) @@ -353,89 +358,91 @@ class CollectionItemMixin(ActivitypubMixin): activity = self.to_add_activity() self.broadcast(activity, self.user) - def delete(self, *args, **kwargs): - ''' broadcast a remove activity ''' + """ broadcast a remove activity """ activity = self.to_remove_activity() super().delete(*args, **kwargs) self.broadcast(activity, self.user) - def to_add_activity(self): - ''' AP for shelving a book''' + """ AP for shelving a book""" object_field = getattr(self, self.object_field) collection_field = getattr(self, self.collection_field) return activitypub.Add( - id='%s#add' % self.remote_id, + id="%s#add" % self.remote_id, actor=self.user.remote_id, object=object_field, - target=collection_field.remote_id + target=collection_field.remote_id, ).serialize() def to_remove_activity(self): - ''' AP for un-shelving a book''' + """ AP for un-shelving a book""" object_field = getattr(self, self.object_field) collection_field = getattr(self, self.collection_field) return activitypub.Remove( - id='%s#remove' % self.remote_id, + id="%s#remove" % self.remote_id, actor=self.user.remote_id, object=object_field, - target=collection_field.remote_id + target=collection_field.remote_id, ).serialize() class ActivityMixin(ActivitypubMixin): - ''' add this mixin for models that are AP serializable ''' + """ add this mixin for models that are AP serializable """ + def save(self, *args, broadcast=True, **kwargs): - ''' broadcast activity ''' + """ broadcast activity """ super().save(*args, **kwargs) - user = self.user if hasattr(self, 'user') else self.user_subject + user = self.user if hasattr(self, "user") else self.user_subject if broadcast and user.local: self.broadcast(self.to_activity(), user) - def delete(self, *args, broadcast=True, **kwargs): - ''' nevermind, undo that activity ''' - user = self.user if hasattr(self, 'user') else self.user_subject + """ nevermind, undo that activity """ + user = self.user if hasattr(self, "user") else self.user_subject if broadcast and user.local: self.broadcast(self.to_undo_activity(), user) super().delete(*args, **kwargs) - def to_undo_activity(self): - ''' undo an action ''' - user = self.user if hasattr(self, 'user') else self.user_subject + """ undo an action """ + user = self.user if hasattr(self, "user") else self.user_subject return activitypub.Undo( - id='%s#undo' % self.remote_id, + id="%s#undo" % self.remote_id, actor=user.remote_id, object=self, ).serialize() def generate_activity(obj): - ''' go through the fields on an object ''' + """ go through the fields on an object """ activity = {} for field in obj.activity_fields: field.set_activity_from_field(activity, obj) - if hasattr(obj, 'serialize_reverse_fields'): + if hasattr(obj, "serialize_reverse_fields"): # for example, editions of a work - for model_field_name, activity_field_name, sort_field in \ - obj.serialize_reverse_fields: + for ( + model_field_name, + activity_field_name, + sort_field, + ) in obj.serialize_reverse_fields: related_field = getattr(obj, model_field_name) - activity[activity_field_name] = \ - unfurl_related_field(related_field, sort_field) + activity[activity_field_name] = unfurl_related_field( + related_field, sort_field + ) - if not activity.get('id'): - activity['id'] = obj.get_remote_id() + if not activity.get("id"): + activity["id"] = obj.get_remote_id() return activity def unfurl_related_field(related_field, sort_field=None): - ''' load reverse lookups (like public key owner or Status attachment ''' - if hasattr(related_field, 'all'): - return [unfurl_related_field(i) for i in related_field.order_by( - sort_field).all()] + """ load reverse lookups (like public key owner or Status attachment """ + if hasattr(related_field, "all"): + return [ + unfurl_related_field(i) for i in related_field.order_by(sort_field).all() + ] if related_field.reverse_unfurl: return related_field.field_to_activity() return related_field.remote_id @@ -443,8 +450,8 @@ def unfurl_related_field(related_field, sort_field=None): @app.task def broadcast_task(sender_id, activity, recipients): - ''' the celery task for broadcast ''' - user_model = apps.get_model('bookwyrm.User', require_ready=True) + """ the celery task for broadcast """ + user_model = apps.get_model("bookwyrm.User", require_ready=True) sender = user_model.objects.get(id=sender_id) for recipient in recipients: try: @@ -454,12 +461,12 @@ def broadcast_task(sender_id, activity, recipients): def sign_and_send(sender, data, destination): - ''' crpyto whatever and http junk ''' + """ crpyto whatever and http junk """ now = http_date() if not sender.key_pair.private_key: # this shouldn't happen. it would be bad if it happened. - raise ValueError('No private key found for sender') + raise ValueError("No private key found for sender") digest = make_digest(data) @@ -467,11 +474,11 @@ def sign_and_send(sender, data, destination): destination, data=data, headers={ - 'Date': now, - 'Digest': digest, - 'Signature': make_signature(sender, destination, now, digest), - 'Content-Type': 'application/activity+json; charset=utf-8', - 'User-Agent': USER_AGENT, + "Date": now, + "Digest": digest, + "Signature": make_signature(sender, destination, now, digest), + "Content-Type": "application/activity+json; charset=utf-8", + "User-Agent": USER_AGENT, }, ) if not response.ok: @@ -481,8 +488,9 @@ def sign_and_send(sender, data, destination): # pylint: disable=unused-argument def to_ordered_collection_page( - queryset, remote_id, id_only=False, page=1, pure=False, **kwargs): - ''' serialize and pagiante a queryset ''' + queryset, remote_id, id_only=False, page=1, pure=False, **kwargs +): + """ serialize and pagiante a queryset """ paginated = Paginator(queryset, PAGE_LENGTH) activity_page = paginated.page(page) @@ -493,14 +501,13 @@ def to_ordered_collection_page( prev_page = next_page = None if activity_page.has_next(): - next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number()) + next_page = "%s?page=%d" % (remote_id, activity_page.next_page_number()) if activity_page.has_previous(): - prev_page = '%s?page=%d' % \ - (remote_id, activity_page.previous_page_number()) + prev_page = "%s?page=%d" % (remote_id, activity_page.previous_page_number()) return activitypub.OrderedCollectionPage( - id='%s?page=%s' % (remote_id, page), + id="%s?page=%s" % (remote_id, page), partOf=remote_id, orderedItems=items, next=next_page, - prev=prev_page + prev=prev_page, ) diff --git a/bookwyrm/models/attachment.py b/bookwyrm/models/attachment.py index e3450a5ad..0cd2c111f 100644 --- a/bookwyrm/models/attachment.py +++ b/bookwyrm/models/attachment.py @@ -1,4 +1,4 @@ -''' media that is posted in the app ''' +""" media that is posted in the app """ from django.db import models from bookwyrm import activitypub @@ -8,23 +8,25 @@ from . import fields class Attachment(ActivitypubMixin, BookWyrmModel): - ''' an image (or, in the future, video etc) associated with a status ''' + """ an image (or, in the future, video etc) associated with a status """ + status = models.ForeignKey( - 'Status', - on_delete=models.CASCADE, - related_name='attachments', - null=True + "Status", on_delete=models.CASCADE, related_name="attachments", null=True ) reverse_unfurl = True + class Meta: - ''' one day we'll have other types of attachments besides images ''' + """ one day we'll have other types of attachments besides images """ + abstract = True class Image(Attachment): - ''' an image attachment ''' + """ an image attachment """ + image = fields.ImageField( - upload_to='status/', null=True, blank=True, activitypub_field='url') - caption = fields.TextField(null=True, blank=True, activitypub_field='name') + upload_to="status/", null=True, blank=True, activitypub_field="url" + ) + caption = fields.TextField(null=True, blank=True, activitypub_field="name") activity_serializer = activitypub.Image diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index d0cb8d19b..4c5fe6c8f 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -1,4 +1,4 @@ -''' database schema for info about authors ''' +""" database schema for info about authors """ from django.db import models from bookwyrm import activitypub @@ -9,9 +9,11 @@ from . import fields class Author(BookDataModel): - ''' basic biographic info ''' + """ basic biographic info """ + wikipedia_link = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) # idk probably other keys would be useful here? born = fields.DateTimeField(blank=True, null=True) died = fields.DateTimeField(blank=True, null=True) @@ -22,7 +24,7 @@ class Author(BookDataModel): bio = fields.HtmlField(null=True, blank=True) def get_remote_id(self): - ''' editions and works both use "book" instead of model_name ''' - return 'https://%s/author/%s' % (DOMAIN, self.id) + """ editions and works both use "book" instead of model_name """ + return "https://%s/author/%s" % (DOMAIN, self.id) activity_serializer = activitypub.Author diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 7af487492..60e5da0ad 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -1,4 +1,4 @@ -''' base model with default fields ''' +""" base model with default fields """ from django.db import models from django.dispatch import receiver @@ -7,34 +7,36 @@ from .fields import RemoteIdField class BookWyrmModel(models.Model): - ''' shared fields ''' + """ shared fields """ + created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) - remote_id = RemoteIdField(null=True, activitypub_field='id') + remote_id = RemoteIdField(null=True, activitypub_field="id") def get_remote_id(self): - ''' generate a url that resolves to the local object ''' - base_path = 'https://%s' % DOMAIN - if hasattr(self, 'user'): - base_path = '%s%s' % (base_path, self.user.local_path) + """ generate a url that resolves to the local object """ + base_path = "https://%s" % DOMAIN + if hasattr(self, "user"): + base_path = "%s%s" % (base_path, self.user.local_path) model_name = type(self).__name__.lower() - return '%s/%s/%d' % (base_path, model_name, self.id) + return "%s/%s/%d" % (base_path, model_name, self.id) class Meta: - ''' this is just here to provide default fields for other models ''' + """ this is just here to provide default fields for other models """ + abstract = True @property def local_path(self): - ''' how to link to this object in the local app ''' - return self.get_remote_id().replace('https://%s' % DOMAIN, '') + """ how to link to this object in the local app """ + return self.get_remote_id().replace("https://%s" % DOMAIN, "") @receiver(models.signals.post_save) -#pylint: disable=unused-argument +# pylint: disable=unused-argument def execute_after_save(sender, instance, created, *args, **kwargs): - ''' set the remote_id after save (when the id is available) ''' - if not created or not hasattr(instance, 'get_remote_id'): + """ set the remote_id after save (when the id is available) """ + if not created or not hasattr(instance, "get_remote_id"): return if not instance.remote_id: instance.remote_id = instance.get_remote_id() diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 84bfbc6bd..66b539bbc 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -1,4 +1,4 @@ -''' database schema for books and shelves ''' +""" database schema for books and shelves """ import re from django.db import models @@ -11,25 +11,30 @@ from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .base_model import BookWyrmModel from . import fields + class BookDataModel(ObjectMixin, BookWyrmModel): - ''' fields shared between editable book data (books, works, authors) ''' + """ fields shared between editable book data (books, works, authors) """ + origin_id = models.CharField(max_length=255, null=True, blank=True) openlibrary_key = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) librarything_key = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) goodreads_key = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) - last_edited_by = models.ForeignKey( - 'User', on_delete=models.PROTECT, null=True) + last_edited_by = models.ForeignKey("User", on_delete=models.PROTECT, null=True) class Meta: - ''' can't initialize this model, that wouldn't make sense ''' + """ can't initialize this model, that wouldn't make sense """ + abstract = True def save(self, *args, **kwargs): - ''' ensure that the remote_id is within this instance ''' + """ ensure that the remote_id is within this instance """ if self.id: self.remote_id = self.get_remote_id() else: @@ -37,15 +42,15 @@ class BookDataModel(ObjectMixin, BookWyrmModel): self.remote_id = None return super().save(*args, **kwargs) - def broadcast(self, activity, sender, software='bookwyrm'): - ''' only send book data updates to other bookwyrm instances ''' + def broadcast(self, activity, sender, software="bookwyrm"): + """ only send book data updates to other bookwyrm instances """ super().broadcast(activity, sender, software=software) class Book(BookDataModel): - ''' a generic book, which can mean either an edition or a work ''' - connector = models.ForeignKey( - 'Connector', on_delete=models.PROTECT, null=True) + """ a generic book, which can mean either an edition or a work """ + + connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True) # book/work metadata title = fields.CharField(max_length=255) @@ -63,9 +68,10 @@ class Book(BookDataModel): subject_places = fields.ArrayField( models.CharField(max_length=255), blank=True, null=True, default=list ) - authors = fields.ManyToManyField('Author') + authors = fields.ManyToManyField("Author") cover = fields.ImageField( - upload_to='covers/', blank=True, null=True, alt_field='alt_text') + upload_to="covers/", blank=True, null=True, alt_field="alt_text" + ) first_published_date = fields.DateTimeField(blank=True, null=True) published_date = fields.DateTimeField(blank=True, null=True) @@ -73,42 +79,43 @@ class Book(BookDataModel): @property def author_text(self): - ''' format a list of authors ''' - return ', '.join(a.name for a in self.authors.all()) + """ format a list of authors """ + return ", ".join(a.name for a in self.authors.all()) @property def latest_readthrough(self): - ''' most recent readthrough activity ''' - return self.readthrough_set.order_by('-updated_date').first() + """ most recent readthrough activity """ + return self.readthrough_set.order_by("-updated_date").first() @property def edition_info(self): - ''' properties of this edition, as a string ''' + """ properties of this edition, as a string """ items = [ - self.physical_format if hasattr(self, 'physical_format') else None, - self.languages[0] + ' language' if self.languages and \ - self.languages[0] != 'English' else None, + self.physical_format if hasattr(self, "physical_format") else None, + self.languages[0] + " language" + if self.languages and self.languages[0] != "English" + else None, str(self.published_date.year) if self.published_date else None, ] - return ', '.join(i for i in items if i) + return ", ".join(i for i in items if i) @property def alt_text(self): - ''' image alt test ''' - text = '%s' % self.title + """ image alt test """ + text = "%s" % self.title if self.edition_info: - text += ' (%s)' % self.edition_info + text += " (%s)" % self.edition_info return text def save(self, *args, **kwargs): - ''' can't be abstract for query reasons, but you shouldn't USE it ''' + """ can't be abstract for query reasons, but you shouldn't USE it """ if not isinstance(self, Edition) and not isinstance(self, Work): - raise ValueError('Books should be added as Editions or Works') + raise ValueError("Books should be added as Editions or Works") return super().save(*args, **kwargs) def get_remote_id(self): - ''' editions and works both use "book" instead of model_name ''' - return 'https://%s/book/%d' % (DOMAIN, self.id) + """ editions and works both use "book" instead of model_name """ + return "https://%s/book/%d" % (DOMAIN, self.id) def __repr__(self): return "<{} key={!r} title={!r}>".format( @@ -119,76 +126,82 @@ class Book(BookDataModel): class Work(OrderedCollectionPageMixin, Book): - ''' a work (an abstract concept of a book that manifests in an edition) ''' + """ a work (an abstract concept of a book that manifests in an edition) """ + # library of congress catalog control number lccn = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) # this has to be nullable but should never be null default_edition = fields.ForeignKey( - 'Edition', - on_delete=models.PROTECT, - null=True, - load_remote=False + "Edition", on_delete=models.PROTECT, null=True, load_remote=False ) def save(self, *args, **kwargs): - ''' set some fields on the edition object ''' + """ set some fields on the edition object """ # set rank for edition in self.editions.all(): edition.save() return super().save(*args, **kwargs) def get_default_edition(self): - ''' in case the default edition is not set ''' - return self.default_edition or self.editions.order_by( - '-edition_rank' - ).first() + """ in case the default edition is not set """ + return self.default_edition or self.editions.order_by("-edition_rank").first() def to_edition_list(self, **kwargs): - ''' an ordered collection of editions ''' + """ an ordered collection of editions """ return self.to_ordered_collection( - self.editions.order_by('-edition_rank').all(), - remote_id='%s/editions' % self.remote_id, + self.editions.order_by("-edition_rank").all(), + remote_id="%s/editions" % self.remote_id, **kwargs ) activity_serializer = activitypub.Work - serialize_reverse_fields = [('editions', 'editions', '-edition_rank')] - deserialize_reverse_fields = [('editions', 'editions')] + serialize_reverse_fields = [("editions", "editions", "-edition_rank")] + deserialize_reverse_fields = [("editions", "editions")] class Edition(Book): - ''' an edition of a book ''' + """ an edition of a book """ + # these identifiers only apply to editions, not works isbn_10 = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) isbn_13 = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) oclc_number = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) asin = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) pages = fields.IntegerField(blank=True, null=True) physical_format = fields.CharField(max_length=255, blank=True, null=True) publishers = fields.ArrayField( models.CharField(max_length=255), blank=True, default=list ) shelves = models.ManyToManyField( - 'Shelf', + "Shelf", symmetrical=False, - through='ShelfBook', - through_fields=('book', 'shelf') + through="ShelfBook", + through_fields=("book", "shelf"), ) parent_work = fields.ForeignKey( - 'Work', on_delete=models.PROTECT, null=True, - related_name='editions', activitypub_field='work') + "Work", + on_delete=models.PROTECT, + null=True, + related_name="editions", + activitypub_field="work", + ) edition_rank = fields.IntegerField(default=0) activity_serializer = activitypub.Edition - name_field = 'title' + name_field = "title" def get_rank(self): - ''' calculate how complete the data is on this edition ''' + """ calculate how complete the data is on this edition """ if self.parent_work and self.parent_work.default_edition == self: # default edition has the highest rank return 20 @@ -204,9 +217,9 @@ class Edition(Book): return rank def save(self, *args, **kwargs): - ''' set some fields on the edition object ''' + """ set some fields on the edition object """ # calculate isbn 10/13 - if self.isbn_13 and self.isbn_13[:3] == '978' and not self.isbn_10: + if self.isbn_13 and self.isbn_13[:3] == "978" and not self.isbn_10: self.isbn_10 = isbn_13_to_10(self.isbn_13) if self.isbn_10 and not self.isbn_13: self.isbn_13 = isbn_10_to_13(self.isbn_10) @@ -218,17 +231,18 @@ class Edition(Book): def isbn_10_to_13(isbn_10): - ''' convert an isbn 10 into an isbn 13 ''' - isbn_10 = re.sub(r'[^0-9X]', '', isbn_10) + """ convert an isbn 10 into an isbn 13 """ + isbn_10 = re.sub(r"[^0-9X]", "", isbn_10) # drop the last character of the isbn 10 number (the original checkdigit) converted = isbn_10[:9] # add "978" to the front - converted = '978' + converted + converted = "978" + converted # add a check digit to the end # multiply the odd digits by 1 and the even digits by 3 and sum them try: - checksum = sum(int(i) for i in converted[::2]) + \ - sum(int(i) * 3 for i in converted[1::2]) + checksum = sum(int(i) for i in converted[::2]) + sum( + int(i) * 3 for i in converted[1::2] + ) except ValueError: return None # add the checksum mod 10 to the end @@ -239,11 +253,11 @@ def isbn_10_to_13(isbn_10): def isbn_13_to_10(isbn_13): - ''' convert isbn 13 to 10, if possible ''' - if isbn_13[:3] != '978': + """ convert isbn 13 to 10, if possible """ + if isbn_13[:3] != "978": return None - isbn_13 = re.sub(r'[^0-9X]', '', isbn_13) + isbn_13 = re.sub(r"[^0-9X]", "", isbn_13) # remove '978' and old checkdigit converted = isbn_13[3:-1] @@ -256,5 +270,5 @@ def isbn_13_to_10(isbn_13): checkdigit = checksum % 11 checkdigit = 11 - checkdigit if checkdigit == 10: - checkdigit = 'X' + checkdigit = "X" return converted + str(checkdigit) diff --git a/bookwyrm/models/connector.py b/bookwyrm/models/connector.py index c1fbf58bc..11bdbee20 100644 --- a/bookwyrm/models/connector.py +++ b/bookwyrm/models/connector.py @@ -1,21 +1,21 @@ -''' manages interfaces with external sources of book data ''' +""" manages interfaces with external sources of book data """ from django.db import models from bookwyrm.connectors.settings import CONNECTORS from .base_model import BookWyrmModel -ConnectorFiles = models.TextChoices('ConnectorFiles', CONNECTORS) +ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS) + + class Connector(BookWyrmModel): - ''' book data source connectors ''' + """ book data source connectors """ + identifier = models.CharField(max_length=255, unique=True) priority = models.IntegerField(default=2) name = models.CharField(max_length=255, null=True, blank=True) local = models.BooleanField(default=False) - connector_file = models.CharField( - max_length=255, - choices=ConnectorFiles.choices - ) + connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices) api_key = models.CharField(max_length=255, null=True, blank=True) base_url = models.CharField(max_length=255) @@ -24,7 +24,7 @@ class Connector(BookWyrmModel): search_url = models.CharField(max_length=255, null=True, blank=True) isbn_search_url = models.CharField(max_length=255, null=True, blank=True) - politeness_delay = models.IntegerField(null=True, blank=True) #seconds + politeness_delay = models.IntegerField(null=True, blank=True) # seconds max_query_count = models.IntegerField(null=True, blank=True) # how many queries executed in a unit of time, like a day query_count = models.IntegerField(default=0) @@ -32,11 +32,12 @@ class Connector(BookWyrmModel): query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True) class Meta: - ''' check that there's code to actually use this connector ''' + """ check that there's code to actually use this connector """ + constraints = [ models.CheckConstraint( check=models.Q(connector_file__in=ConnectorFiles), - name='connector_file_valid' + name="connector_file_valid", ) ] diff --git a/bookwyrm/models/favorite.py b/bookwyrm/models/favorite.py index d34cbcba8..7b72d175f 100644 --- a/bookwyrm/models/favorite.py +++ b/bookwyrm/models/favorite.py @@ -1,4 +1,4 @@ -''' like/fav/star a status ''' +""" like/fav/star a status """ from django.apps import apps from django.db import models from django.utils import timezone @@ -9,50 +9,59 @@ from .base_model import BookWyrmModel from . import fields from .status import Status + class Favorite(ActivityMixin, BookWyrmModel): - ''' fav'ing a post ''' + """ fav'ing a post """ + user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='actor') + "User", on_delete=models.PROTECT, activitypub_field="actor" + ) status = fields.ForeignKey( - 'Status', on_delete=models.PROTECT, activitypub_field='object') + "Status", on_delete=models.PROTECT, activitypub_field="object" + ) activity_serializer = activitypub.Like @classmethod def ignore_activity(cls, activity): - ''' don't bother with incoming favs of unknown statuses ''' + """ don't bother with incoming favs of unknown statuses """ return not Status.objects.filter(remote_id=activity.object).exists() def save(self, *args, **kwargs): - ''' update user active time ''' + """ update user active time """ self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) if self.status.user.local and self.status.user != self.user: notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + "bookwyrm.Notification", require_ready=True + ) notification_model.objects.create( user=self.status.user, - notification_type='FAVORITE', + notification_type="FAVORITE", related_user=self.user, - related_status=self.status + related_status=self.status, ) def delete(self, *args, **kwargs): - ''' delete and delete notifications ''' + """ delete and delete notifications """ # check for notification if self.status.user.local: notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + "bookwyrm.Notification", require_ready=True + ) notification = notification_model.objects.filter( - user=self.status.user, related_user=self.user, - related_status=self.status, notification_type='FAVORITE' + user=self.status.user, + related_user=self.user, + related_status=self.status, + notification_type="FAVORITE", ).first() if notification: notification.delete() super().delete(*args, **kwargs) class Meta: - ''' can't fav things twice ''' - unique_together = ('user', 'status') + """ can't fav things twice """ + + unique_together = ("user", "status") diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index 953cd9c8a..ce8043102 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -1,15 +1,17 @@ -''' connections to external ActivityPub servers ''' +""" connections to external ActivityPub servers """ from django.db import models from .base_model import BookWyrmModel class FederatedServer(BookWyrmModel): - ''' store which server's we federate with ''' + """ store which server's we federate with """ + server_name = models.CharField(max_length=255, unique=True) # federated, blocked, whatever else - status = models.CharField(max_length=255, default='federated') + status = models.CharField(max_length=255, default="federated") # is it mastodon, bookwyrm, etc application_type = models.CharField(max_length=255, null=True) application_version = models.CharField(max_length=255, null=True) + # TODO: blocked servers diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 4ea527eba..1ca0b377b 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -1,4 +1,4 @@ -''' activitypub-aware django model fields ''' +""" activitypub-aware django model fields """ from dataclasses import MISSING import re from uuid import uuid4 @@ -18,37 +18,43 @@ from bookwyrm.settings import DOMAIN def validate_remote_id(value): - ''' make sure the remote_id looks like a url ''' - if not value or not re.match(r'^http.?:\/\/[^\s]+$', value): + """ make sure the remote_id looks like a url """ + if not value or not re.match(r"^http.?:\/\/[^\s]+$", value): raise ValidationError( - _('%(value)s is not a valid remote_id'), - params={'value': value}, + _("%(value)s is not a valid remote_id"), + params={"value": value}, ) def validate_localname(value): - ''' make sure localnames look okay ''' - if not re.match(r'^[A-Za-z\-_\.0-9]+$', value): + """ make sure localnames look okay """ + if not re.match(r"^[A-Za-z\-_\.0-9]+$", value): raise ValidationError( - _('%(value)s is not a valid username'), - params={'value': value}, + _("%(value)s is not a valid username"), + params={"value": value}, ) def validate_username(value): - ''' make sure usernames look okay ''' - if not re.match(r'^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$', value): + """ make sure usernames look okay """ + if not re.match(r"^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$", value): raise ValidationError( - _('%(value)s is not a valid username'), - params={'value': value}, + _("%(value)s is not a valid username"), + params={"value": value}, ) class ActivitypubFieldMixin: - ''' make a database field serializable ''' - def __init__(self, *args, \ - activitypub_field=None, activitypub_wrapper=None, - deduplication_field=False, **kwargs): + """ make a database field serializable """ + + def __init__( + self, + *args, + activitypub_field=None, + activitypub_wrapper=None, + deduplication_field=False, + **kwargs + ): self.deduplication_field = deduplication_field if activitypub_wrapper: self.activitypub_wrapper = activitypub_field @@ -57,24 +63,22 @@ class ActivitypubFieldMixin: self.activitypub_field = activitypub_field super().__init__(*args, **kwargs) - def set_field_from_activity(self, instance, data): - ''' helper function for assinging a value to the field ''' + """ helper function for assinging a value to the field """ try: value = getattr(data, self.get_activitypub_field()) except AttributeError: # masssively hack-y workaround for boosts - if self.get_activitypub_field() != 'attributedTo': + if self.get_activitypub_field() != "attributedTo": raise - value = getattr(data, 'actor') + value = getattr(data, "actor") formatted = self.field_from_activity(value) if formatted is None or formatted is MISSING: return setattr(instance, self.name, formatted) - def set_activity_from_field(self, activity, instance): - ''' update the json object ''' + """ update the json object """ value = getattr(instance, self.name) formatted = self.field_to_activity(value) if formatted is None: @@ -82,37 +86,37 @@ class ActivitypubFieldMixin: key = self.get_activitypub_field() # TODO: surely there's a better way - if instance.__class__.__name__ == 'Boost' and key == 'attributedTo': - key = 'actor' + if instance.__class__.__name__ == "Boost" and key == "attributedTo": + key = "actor" if isinstance(activity.get(key), list): activity[key] += formatted else: activity[key] = formatted - def field_to_activity(self, value): - ''' formatter to convert a model value into activitypub ''' - if hasattr(self, 'activitypub_wrapper'): + """ formatter to convert a model value into activitypub """ + if hasattr(self, "activitypub_wrapper"): return {self.activitypub_wrapper: value} return value def field_from_activity(self, value): - ''' formatter to convert activitypub into a model value ''' - if hasattr(self, 'activitypub_wrapper'): + """ formatter to convert activitypub into a model value """ + if hasattr(self, "activitypub_wrapper"): value = value.get(self.activitypub_wrapper) return value def get_activitypub_field(self): - ''' model_field_name to activitypubFieldName ''' + """ model_field_name to activitypubFieldName """ if self.activitypub_field: return self.activitypub_field - name = self.name.split('.')[-1] - components = name.split('_') - return components[0] + ''.join(x.title() for x in components[1:]) + name = self.name.split(".")[-1] + components = name.split("_") + return components[0] + "".join(x.title() for x in components[1:]) class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): - ''' default (de)serialization for foreign key and one to one ''' + """ default (de)serialization for foreign key and one to one """ + def __init__(self, *args, load_remote=True, **kwargs): self.load_remote = load_remote super().__init__(*args, **kwargs) @@ -122,7 +126,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): return None related_model = self.related_model - if hasattr(value, 'id') and value.id: + if hasattr(value, "id") and value.id: if not self.load_remote: # only look in the local database return related_model.find_existing(value.serialize()) @@ -142,99 +146,98 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): class RemoteIdField(ActivitypubFieldMixin, models.CharField): - ''' a url that serves as a unique identifier ''' + """ a url that serves as a unique identifier """ + def __init__(self, *args, max_length=255, validators=None, **kwargs): validators = validators or [validate_remote_id] - super().__init__( - *args, max_length=max_length, validators=validators, - **kwargs - ) + super().__init__(*args, max_length=max_length, validators=validators, **kwargs) # for this field, the default is true. false everywhere else. - self.deduplication_field = kwargs.get('deduplication_field', True) + self.deduplication_field = kwargs.get("deduplication_field", True) class UsernameField(ActivitypubFieldMixin, models.CharField): - ''' activitypub-aware username field ''' - def __init__(self, activitypub_field='preferredUsername', **kwargs): + """ activitypub-aware username field """ + + def __init__(self, activitypub_field="preferredUsername", **kwargs): self.activitypub_field = activitypub_field # I don't totally know why pylint is mad at this, but it makes it work - super( #pylint: disable=bad-super-call - ActivitypubFieldMixin, self - ).__init__( - _('username'), + super(ActivitypubFieldMixin, self).__init__( # pylint: disable=bad-super-call + _("username"), max_length=150, unique=True, validators=[validate_username], error_messages={ - 'unique': _('A user with that username already exists.'), + "unique": _("A user with that username already exists."), }, ) def deconstruct(self): - ''' implementation of models.Field deconstruct ''' + """ implementation of models.Field deconstruct """ name, path, args, kwargs = super().deconstruct() - del kwargs['verbose_name'] - del kwargs['max_length'] - del kwargs['unique'] - del kwargs['validators'] - del kwargs['error_messages'] + del kwargs["verbose_name"] + del kwargs["max_length"] + del kwargs["unique"] + del kwargs["validators"] + del kwargs["error_messages"] return name, path, args, kwargs def field_to_activity(self, value): - return value.split('@')[0] + return value.split("@")[0] -PrivacyLevels = models.TextChoices('Privacy', [ - 'public', - 'unlisted', - 'followers', - 'direct' -]) +PrivacyLevels = models.TextChoices( + "Privacy", ["public", "unlisted", "followers", "direct"] +) + class PrivacyField(ActivitypubFieldMixin, models.CharField): - ''' this maps to two differente activitypub fields ''' - public = 'https://www.w3.org/ns/activitystreams#Public' + """ this maps to two differente activitypub fields """ + + public = "https://www.w3.org/ns/activitystreams#Public" + def __init__(self, *args, **kwargs): super().__init__( - *args, max_length=255, - choices=PrivacyLevels.choices, default='public') + *args, max_length=255, choices=PrivacyLevels.choices, default="public" + ) def set_field_from_activity(self, instance, data): to = data.to cc = data.cc if to == [self.public]: - setattr(instance, self.name, 'public') + setattr(instance, self.name, "public") elif cc == []: - setattr(instance, self.name, 'direct') + setattr(instance, self.name, "direct") elif self.public in cc: - setattr(instance, self.name, 'unlisted') + setattr(instance, self.name, "unlisted") else: - setattr(instance, self.name, 'followers') + setattr(instance, self.name, "followers") def set_activity_from_field(self, activity, instance): # explicitly to anyone mentioned (statuses only) mentions = [] - if hasattr(instance, 'mention_users'): + if hasattr(instance, "mention_users"): mentions = [u.remote_id for u in instance.mention_users.all()] # this is a link to the followers list - followers = instance.user.__class__._meta.get_field('followers')\ - .field_to_activity(instance.user.followers) - if instance.privacy == 'public': - activity['to'] = [self.public] - activity['cc'] = [followers] + mentions - elif instance.privacy == 'unlisted': - activity['to'] = [followers] - activity['cc'] = [self.public] + mentions - elif instance.privacy == 'followers': - activity['to'] = [followers] - activity['cc'] = mentions - if instance.privacy == 'direct': - activity['to'] = mentions - activity['cc'] = [] + followers = instance.user.__class__._meta.get_field( + "followers" + ).field_to_activity(instance.user.followers) + if instance.privacy == "public": + activity["to"] = [self.public] + activity["cc"] = [followers] + mentions + elif instance.privacy == "unlisted": + activity["to"] = [followers] + activity["cc"] = [self.public] + mentions + elif instance.privacy == "followers": + activity["to"] = [followers] + activity["cc"] = mentions + if instance.privacy == "direct": + activity["to"] = mentions + activity["cc"] = [] class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): - ''' activitypub-aware foreign key field ''' + """ activitypub-aware foreign key field """ + def field_to_activity(self, value): if not value: return None @@ -242,7 +245,8 @@ class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): - ''' activitypub-aware foreign key field ''' + """ activitypub-aware foreign key field """ + def field_to_activity(self, value): if not value: return None @@ -250,13 +254,14 @@ class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): - ''' activitypub-aware many to many field ''' + """ activitypub-aware many to many field """ + def __init__(self, *args, link_only=False, **kwargs): self.link_only = link_only super().__init__(*args, **kwargs) def set_field_from_activity(self, instance, data): - ''' helper function for assinging a value to the field ''' + """ helper function for assinging a value to the field """ value = getattr(data, self.get_activitypub_field()) formatted = self.field_from_activity(value) if formatted is None or formatted is MISSING: @@ -266,7 +271,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): def field_to_activity(self, value): if self.link_only: - return '%s/%s' % (value.instance.remote_id, self.name) + return "%s/%s" % (value.instance.remote_id, self.name) return [i.remote_id for i in value.all()] def field_from_activity(self, value): @@ -279,29 +284,31 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): except ValidationError: continue items.append( - activitypub.resolve_remote_id( - remote_id, model=self.related_model) + activitypub.resolve_remote_id(remote_id, model=self.related_model) ) return items class TagField(ManyToManyField): - ''' special case of many to many that uses Tags ''' + """ special case of many to many that uses Tags """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.activitypub_field = 'tag' + self.activitypub_field = "tag" def field_to_activity(self, value): tags = [] for item in value.all(): activity_type = item.__class__.__name__ - if activity_type == 'User': - activity_type = 'Mention' - tags.append(activitypub.Link( - href=item.remote_id, - name=getattr(item, item.name_field), - type=activity_type - )) + if activity_type == "User": + activity_type = "Mention" + tags.append( + activitypub.Link( + href=item.remote_id, + name=getattr(item, item.name_field), + type=activity_type, + ) + ) return tags def field_from_activity(self, value): @@ -310,38 +317,38 @@ class TagField(ManyToManyField): items = [] for link_json in value: link = activitypub.Link(**link_json) - tag_type = link.type if link.type != 'Mention' else 'Person' - if tag_type == 'Book': - tag_type = 'Edition' + tag_type = link.type if link.type != "Mention" else "Person" + if tag_type == "Book": + tag_type = "Edition" if tag_type != self.related_model.activity_serializer.type: # tags can contain multiple types continue items.append( - activitypub.resolve_remote_id( - link.href, model=self.related_model) + activitypub.resolve_remote_id(link.href, model=self.related_model) ) return items def image_serializer(value, alt): - ''' helper for serializing images ''' - if value and hasattr(value, 'url'): + """ helper for serializing images """ + if value and hasattr(value, "url"): url = value.url else: return None - url = 'https://%s%s' % (DOMAIN, url) + url = "https://%s%s" % (DOMAIN, url) return activitypub.Image(url=url, name=alt) class ImageField(ActivitypubFieldMixin, models.ImageField): - ''' activitypub-aware image field ''' + """ activitypub-aware image field """ + def __init__(self, *args, alt_field=None, **kwargs): self.alt_field = alt_field super().__init__(*args, **kwargs) # pylint: disable=arguments-differ def set_field_from_activity(self, instance, data, save=True): - ''' helper function for assinging a value to the field ''' + """ helper function for assinging a value to the field """ value = getattr(data, self.get_activitypub_field()) formatted = self.field_from_activity(value) if formatted is None or formatted is MISSING: @@ -358,16 +365,14 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): key = self.get_activitypub_field() activity[key] = formatted - def field_to_activity(self, value, alt=None): return image_serializer(value, alt) - def field_from_activity(self, value): image_slug = value # when it's an inline image (User avatar/icon, Book cover), it's a json # blob, but when it's an attached image, it's just a url - if hasattr(image_slug, 'url'): + if hasattr(image_slug, "url"): url = image_slug.url elif isinstance(image_slug, str): url = image_slug @@ -383,13 +388,14 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): if not response: return None - image_name = str(uuid4()) + '.' + url.split('.')[-1] + image_name = str(uuid4()) + "." + url.split(".")[-1] image_content = ContentFile(response.content) return [image_name, image_content] class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): - ''' activitypub-aware datetime field ''' + """ activitypub-aware datetime field """ + def field_to_activity(self, value): if not value: return None @@ -405,8 +411,10 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): except (ParserError, TypeError): return None + class HtmlField(ActivitypubFieldMixin, models.TextField): - ''' a text field for storing html ''' + """ a text field for storing html """ + def field_from_activity(self, value): if not value or value == MISSING: return None @@ -414,19 +422,25 @@ class HtmlField(ActivitypubFieldMixin, models.TextField): sanitizer.feed(value) return sanitizer.get_output() + class ArrayField(ActivitypubFieldMixin, DjangoArrayField): - ''' activitypub-aware array field ''' + """ activitypub-aware array field """ + def field_to_activity(self, value): return [str(i) for i in value] + class CharField(ActivitypubFieldMixin, models.CharField): - ''' activitypub-aware char field ''' + """ activitypub-aware char field """ + class TextField(ActivitypubFieldMixin, models.TextField): - ''' activitypub-aware text field ''' + """ activitypub-aware text field """ + class BooleanField(ActivitypubFieldMixin, models.BooleanField): - ''' activitypub-aware boolean field ''' + """ activitypub-aware boolean field """ + class IntegerField(ActivitypubFieldMixin, models.IntegerField): - ''' activitypub-aware boolean field ''' + """ activitypub-aware boolean field """ diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index ca05ddb08..31dccda8f 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -1,4 +1,4 @@ -''' track progress of goodreads imports ''' +""" track progress of goodreads imports """ import re import dateutil.parser @@ -14,13 +14,14 @@ from .fields import PrivacyLevels # Mapping goodreads -> bookwyrm shelf titles. GOODREADS_SHELVES = { - 'read': 'read', - 'currently-reading': 'reading', - 'to-read': 'to-read', + "read": "read", + "currently-reading": "reading", + "to-read": "to-read", } + def unquote_string(text): - ''' resolve csv quote weirdness ''' + """ resolve csv quote weirdness """ match = re.match(r'="([^"]*)"', text) if match: return match.group(1) @@ -28,63 +29,57 @@ def unquote_string(text): def construct_search_term(title, author): - ''' formulate a query for the data connector ''' + """ formulate a query for the data connector """ # Strip brackets (usually series title from search term) - title = re.sub(r'\s*\([^)]*\)\s*', '', title) + title = re.sub(r"\s*\([^)]*\)\s*", "", title) # Open library doesn't like including author initials in search term. - author = re.sub(r'(\w\.)+\s*', '', author) + author = re.sub(r"(\w\.)+\s*", "", author) - return ' '.join([title, author]) + return " ".join([title, author]) class ImportJob(models.Model): - ''' entry for a specific request for book data import ''' + """ entry for a specific request for book data import """ + user = models.ForeignKey(User, on_delete=models.CASCADE) created_date = models.DateTimeField(default=timezone.now) task_id = models.CharField(max_length=100, null=True) include_reviews = models.BooleanField(default=True) complete = models.BooleanField(default=False) privacy = models.CharField( - max_length=255, - default='public', - choices=PrivacyLevels.choices + max_length=255, default="public", choices=PrivacyLevels.choices ) retry = models.BooleanField(default=False) def save(self, *args, **kwargs): - ''' save and notify ''' + """ save and notify """ super().save(*args, **kwargs) if self.complete: notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + "bookwyrm.Notification", require_ready=True + ) notification_model.objects.create( user=self.user, - notification_type='IMPORT', + notification_type="IMPORT", related_import=self, ) class ImportItem(models.Model): - ''' a single line of a csv being imported ''' - job = models.ForeignKey( - ImportJob, - on_delete=models.CASCADE, - related_name='items') + """ a single line of a csv being imported """ + + job = models.ForeignKey(ImportJob, on_delete=models.CASCADE, related_name="items") index = models.IntegerField() data = JSONField() - book = models.ForeignKey( - Book, on_delete=models.SET_NULL, null=True, blank=True) + book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True) fail_reason = models.TextField(null=True) def resolve(self): - ''' try various ways to lookup a book ''' - self.book = ( - self.get_book_from_isbn() or - self.get_book_from_title_author() - ) + """ try various ways to lookup a book """ + self.book = self.get_book_from_isbn() or self.get_book_from_title_author() def get_book_from_isbn(self): - ''' search by isbn ''' + """ search by isbn """ search_result = connector_manager.first_search_result( self.isbn, min_confidence=0.999 ) @@ -93,13 +88,9 @@ class ImportItem(models.Model): return search_result.connector.get_or_create_book(search_result.key) return None - def get_book_from_title_author(self): - ''' search by title and author ''' - search_term = construct_search_term( - self.title, - self.author - ) + """ search by title and author """ + search_term = construct_search_term(self.title, self.author) search_result = connector_manager.first_search_result( search_term, min_confidence=0.999 ) @@ -108,84 +99,85 @@ class ImportItem(models.Model): return search_result.connector.get_or_create_book(search_result.key) return None - @property def title(self): - ''' get the book title ''' - return self.data['Title'] + """ get the book title """ + return self.data["Title"] @property def author(self): - ''' get the book title ''' - return self.data['Author'] + """ get the book title """ + return self.data["Author"] @property def isbn(self): - ''' pulls out the isbn13 field from the csv line data ''' - return unquote_string(self.data['ISBN13']) + """ pulls out the isbn13 field from the csv line data """ + return unquote_string(self.data["ISBN13"]) @property def shelf(self): - ''' the goodreads shelf field ''' - if self.data['Exclusive Shelf']: - return GOODREADS_SHELVES.get(self.data['Exclusive Shelf']) + """ the goodreads shelf field """ + if self.data["Exclusive Shelf"]: + return GOODREADS_SHELVES.get(self.data["Exclusive Shelf"]) return None @property def review(self): - ''' a user-written review, to be imported with the book data ''' - return self.data['My Review'] + """ a user-written review, to be imported with the book data """ + return self.data["My Review"] @property def rating(self): - ''' x/5 star rating for a book ''' - return int(self.data['My Rating']) + """ x/5 star rating for a book """ + return int(self.data["My Rating"]) @property def date_added(self): - ''' when the book was added to this dataset ''' - if self.data['Date Added']: - return timezone.make_aware( - dateutil.parser.parse(self.data['Date Added'])) + """ when the book was added to this dataset """ + if self.data["Date Added"]: + return timezone.make_aware(dateutil.parser.parse(self.data["Date Added"])) return None @property def date_started(self): - ''' when the book was started ''' - if "Date Started" in self.data and self.data['Date Started']: - return timezone.make_aware( - dateutil.parser.parse(self.data['Date Started'])) + """ when the book was started """ + if "Date Started" in self.data and self.data["Date Started"]: + return timezone.make_aware(dateutil.parser.parse(self.data["Date Started"])) return None @property def date_read(self): - ''' the date a book was completed ''' - if self.data['Date Read']: - return timezone.make_aware( - dateutil.parser.parse(self.data['Date Read'])) + """ the date a book was completed """ + if self.data["Date Read"]: + return timezone.make_aware(dateutil.parser.parse(self.data["Date Read"])) return None @property def reads(self): - ''' formats a read through dataset for the book in this line ''' + """ formats a read through dataset for the book in this line """ start_date = self.date_started # Goodreads special case (no 'date started' field) - if ((self.shelf == 'reading' or (self.shelf == 'read' and self.date_read)) - and self.date_added and not start_date): + if ( + (self.shelf == "reading" or (self.shelf == "read" and self.date_read)) + and self.date_added + and not start_date + ): start_date = self.date_added - if (start_date and start_date is not None and not self.date_read): + if start_date and start_date is not None and not self.date_read: return [ReadThrough(start_date=start_date)] if self.date_read: - return [ReadThrough( - start_date=start_date, - finish_date=self.date_read, - )] + return [ + ReadThrough( + start_date=start_date, + finish_date=self.date_read, + ) + ] return [] def __repr__(self): - return "<{!r}Item {!r}>".format(self.data['import_source'], self.data['Title']) + return "<{!r}Item {!r}>".format(self.data["import_source"], self.data["Title"]) def __str__(self): - return "{} by {}".format(self.data['Title'], self.data['Author']) + return "{} by {}".format(self.data["Title"], self.data["Author"]) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 1b14c2aa5..a05325f3f 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -1,4 +1,4 @@ -''' make a list of books!! ''' +""" make a list of books!! """ from django.apps import apps from django.db import models @@ -9,86 +9,89 @@ from .base_model import BookWyrmModel from . import fields -CurationType = models.TextChoices('Curation', [ - 'closed', - 'open', - 'curated', -]) +CurationType = models.TextChoices( + "Curation", + [ + "closed", + "open", + "curated", + ], +) + class List(OrderedCollectionMixin, BookWyrmModel): - ''' a list of books ''' + """ a list of books """ + name = fields.CharField(max_length=100) user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='owner') - description = fields.TextField( - blank=True, null=True, activitypub_field='summary') + "User", on_delete=models.PROTECT, activitypub_field="owner" + ) + description = fields.TextField(blank=True, null=True, activitypub_field="summary") privacy = fields.PrivacyField() curation = fields.CharField( - max_length=255, - default='closed', - choices=CurationType.choices + max_length=255, default="closed", choices=CurationType.choices ) books = models.ManyToManyField( - 'Edition', + "Edition", symmetrical=False, - through='ListItem', - through_fields=('book_list', 'book'), + through="ListItem", + through_fields=("book_list", "book"), ) activity_serializer = activitypub.BookList def get_remote_id(self): - ''' don't want the user to be in there in this case ''' - return 'https://%s/list/%d' % (DOMAIN, self.id) + """ don't want the user to be in there in this case """ + return "https://%s/list/%d" % (DOMAIN, self.id) @property def collection_queryset(self): - ''' list of books for this shelf, overrides OrderedCollectionMixin ''' - return self.books.filter( - listitem__approved=True - ).all().order_by('listitem') + """ list of books for this shelf, overrides OrderedCollectionMixin """ + return self.books.filter(listitem__approved=True).all().order_by("listitem") class Meta: - ''' default sorting ''' - ordering = ('-updated_date',) + """ default sorting """ + + ordering = ("-updated_date",) class ListItem(CollectionItemMixin, BookWyrmModel): - ''' ok ''' + """ ok """ + book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='object') + "Edition", on_delete=models.PROTECT, activitypub_field="object" + ) book_list = fields.ForeignKey( - 'List', on_delete=models.CASCADE, activitypub_field='target') + "List", on_delete=models.CASCADE, activitypub_field="target" + ) user = fields.ForeignKey( - 'User', - on_delete=models.PROTECT, - activitypub_field='actor' + "User", on_delete=models.PROTECT, activitypub_field="actor" ) notes = fields.TextField(blank=True, null=True) approved = models.BooleanField(default=True) order = fields.IntegerField(blank=True, null=True) - endorsement = models.ManyToManyField('User', related_name='endorsers') + endorsement = models.ManyToManyField("User", related_name="endorsers") activity_serializer = activitypub.Add - object_field = 'book' - collection_field = 'book_list' + object_field = "book" + collection_field = "book_list" def save(self, *args, **kwargs): - ''' create a notification too ''' + """ create a notification too """ created = not bool(self.id) super().save(*args, **kwargs) list_owner = self.book_list.user # create a notification if somoene ELSE added to a local user's list if created and list_owner.local and list_owner != self.user: - model = apps.get_model('bookwyrm.Notification', require_ready=True) + model = apps.get_model("bookwyrm.Notification", require_ready=True) model.objects.create( user=list_owner, related_user=self.user, related_list_item=self, - notification_type='ADD', + notification_type="ADD", ) - class Meta: - ''' an opinionated constraint! you can't put a book on a list twice ''' - unique_together = ('book', 'book_list') - ordering = ('-created_date',) + """ an opinionated constraint! you can't put a book on a list twice """ + + unique_together = ("book", "book_list") + ordering = ("-created_date",) diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 0470b3258..190181657 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -1,47 +1,50 @@ -''' alert a user to activity ''' +""" alert a user to activity """ from django.db import models from .base_model import BookWyrmModel NotificationType = models.TextChoices( - 'NotificationType', - 'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD') + "NotificationType", + "FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD", +) + class Notification(BookWyrmModel): - ''' you've been tagged, liked, followed, etc ''' - user = models.ForeignKey('User', on_delete=models.CASCADE) - related_book = models.ForeignKey( - 'Edition', on_delete=models.CASCADE, null=True) + """ you've been tagged, liked, followed, etc """ + + user = models.ForeignKey("User", on_delete=models.CASCADE) + related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True) related_user = models.ForeignKey( - 'User', - on_delete=models.CASCADE, null=True, related_name='related_user') - related_status = models.ForeignKey( - 'Status', on_delete=models.CASCADE, null=True) - related_import = models.ForeignKey( - 'ImportJob', on_delete=models.CASCADE, null=True) + "User", on_delete=models.CASCADE, null=True, related_name="related_user" + ) + related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True) + related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True) related_list_item = models.ForeignKey( - 'ListItem', on_delete=models.CASCADE, null=True) + "ListItem", on_delete=models.CASCADE, null=True + ) read = models.BooleanField(default=False) notification_type = models.CharField( - max_length=255, choices=NotificationType.choices) + max_length=255, choices=NotificationType.choices + ) def save(self, *args, **kwargs): - ''' save, but don't make dupes ''' + """ save, but don't make dupes """ # there's probably a better way to do this if self.__class__.objects.filter( - user=self.user, - related_book=self.related_book, - related_user=self.related_user, - related_status=self.related_status, - related_import=self.related_import, - related_list_item=self.related_list_item, - notification_type=self.notification_type, - ).exists(): + user=self.user, + related_book=self.related_book, + related_user=self.related_user, + related_status=self.related_status, + related_import=self.related_import, + related_list_item=self.related_list_item, + notification_type=self.notification_type, + ).exists(): return super().save(*args, **kwargs) class Meta: - ''' checks if notifcation is in enum list for valid types ''' + """ checks if notifcation is in enum list for valid types """ + constraints = [ models.CheckConstraint( check=models.Q(notification_type__in=NotificationType.values), diff --git a/bookwyrm/models/readthrough.py b/bookwyrm/models/readthrough.py index 2bec3a818..3445573c4 100644 --- a/bookwyrm/models/readthrough.py +++ b/bookwyrm/models/readthrough.py @@ -1,35 +1,32 @@ -''' progress in a book ''' +""" progress in a book """ from django.db import models from django.utils import timezone from django.core import validators from .base_model import BookWyrmModel + class ProgressMode(models.TextChoices): - PAGE = 'PG', 'page' - PERCENT = 'PCT', 'percent' + PAGE = "PG", "page" + PERCENT = "PCT", "percent" + class ReadThrough(BookWyrmModel): - ''' Store a read through a book in the database. ''' - user = models.ForeignKey('User', on_delete=models.PROTECT) - book = models.ForeignKey('Edition', on_delete=models.PROTECT) + """ Store a read through a book in the database. """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + book = models.ForeignKey("Edition", on_delete=models.PROTECT) progress = models.IntegerField( - validators=[validators.MinValueValidator(0)], - null=True, - blank=True) + validators=[validators.MinValueValidator(0)], null=True, blank=True + ) progress_mode = models.CharField( - max_length=3, - choices=ProgressMode.choices, - default=ProgressMode.PAGE) - start_date = models.DateTimeField( - blank=True, - null=True) - finish_date = models.DateTimeField( - blank=True, - null=True) + max_length=3, choices=ProgressMode.choices, default=ProgressMode.PAGE + ) + start_date = models.DateTimeField(blank=True, null=True) + finish_date = models.DateTimeField(blank=True, null=True) def save(self, *args, **kwargs): - ''' update user active time ''' + """ update user active time """ self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) @@ -37,22 +34,22 @@ class ReadThrough(BookWyrmModel): def create_update(self): if self.progress: return self.progressupdate_set.create( - user=self.user, - progress=self.progress, - mode=self.progress_mode) + user=self.user, progress=self.progress, mode=self.progress_mode + ) + class ProgressUpdate(BookWyrmModel): - ''' Store progress through a book in the database. ''' - user = models.ForeignKey('User', on_delete=models.PROTECT) - readthrough = models.ForeignKey('ReadThrough', on_delete=models.CASCADE) + """ Store progress through a book in the database. """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + readthrough = models.ForeignKey("ReadThrough", on_delete=models.CASCADE) progress = models.IntegerField(validators=[validators.MinValueValidator(0)]) mode = models.CharField( - max_length=3, - choices=ProgressMode.choices, - default=ProgressMode.PAGE) + max_length=3, choices=ProgressMode.choices, default=ProgressMode.PAGE + ) def save(self, *args, **kwargs): - ''' update user active time ''' + """ update user active time """ self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 3b0e85d41..df99d2165 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -1,4 +1,4 @@ -''' defines relationships between users ''' +""" defines relationships between users """ from django.apps import apps from django.db import models, transaction, IntegrityError from django.db.models import Q @@ -11,71 +11,74 @@ from . import fields class UserRelationship(BookWyrmModel): - ''' many-to-many through table for followers ''' + """ many-to-many through table for followers """ + user_subject = fields.ForeignKey( - 'User', + "User", on_delete=models.PROTECT, - related_name='%(class)s_user_subject', - activitypub_field='actor', + related_name="%(class)s_user_subject", + activitypub_field="actor", ) user_object = fields.ForeignKey( - 'User', + "User", on_delete=models.PROTECT, - related_name='%(class)s_user_object', - activitypub_field='object', + related_name="%(class)s_user_object", + activitypub_field="object", ) @property def privacy(self): - ''' all relationships are handled directly with the participants ''' - return 'direct' + """ all relationships are handled directly with the participants """ + return "direct" @property def recipients(self): - ''' the remote user needs to recieve direct broadcasts ''' + """ the remote user needs to recieve direct broadcasts """ return [u for u in [self.user_subject, self.user_object] if not u.local] class Meta: - ''' relationships should be unique ''' + """ relationships should be unique """ + abstract = True constraints = [ models.UniqueConstraint( - fields=['user_subject', 'user_object'], - name='%(class)s_unique' + fields=["user_subject", "user_object"], name="%(class)s_unique" ), models.CheckConstraint( - check=~models.Q(user_subject=models.F('user_object')), - name='%(class)s_no_self' - ) + check=~models.Q(user_subject=models.F("user_object")), + name="%(class)s_no_self", + ), ] - def get_remote_id(self, status=None):# pylint: disable=arguments-differ - ''' use shelf identifier in remote_id ''' - status = status or 'follows' + def get_remote_id(self, status=None): # pylint: disable=arguments-differ + """ use shelf identifier in remote_id """ + status = status or "follows" base_path = self.user_subject.remote_id - return '%s#%s/%d' % (base_path, status, self.id) + return "%s#%s/%d" % (base_path, status, self.id) class UserFollows(ActivityMixin, UserRelationship): - ''' Following a user ''' - status = 'follows' + """ Following a user """ + + status = "follows" def to_activity(self): - ''' overrides default to manually set serializer ''' + """ overrides default to manually set serializer """ return activitypub.Follow(**generate_activity(self)) def save(self, *args, **kwargs): - ''' really really don't let a user follow someone who blocked them ''' + """ really really don't let a user follow someone who blocked them """ # blocking in either direction is a no-go if UserBlocks.objects.filter( - Q( - user_subject=self.user_subject, - user_object=self.user_object, - ) | Q( - user_subject=self.user_object, - user_object=self.user_subject, - ) - ).exists(): + Q( + user_subject=self.user_subject, + user_object=self.user_object, + ) + | Q( + user_subject=self.user_object, + user_object=self.user_subject, + ) + ).exists(): raise IntegrityError() # don't broadcast this type of relationship -- accepts and requests # are handled by the UserFollowRequest model @@ -83,7 +86,7 @@ class UserFollows(ActivityMixin, UserRelationship): @classmethod def from_request(cls, follow_request): - ''' converts a follow request into a follow relationship ''' + """ converts a follow request into a follow relationship """ return cls.objects.create( user_subject=follow_request.user_subject, user_object=follow_request.user_object, @@ -92,28 +95,30 @@ class UserFollows(ActivityMixin, UserRelationship): class UserFollowRequest(ActivitypubMixin, UserRelationship): - ''' following a user requires manual or automatic confirmation ''' - status = 'follow_request' + """ following a user requires manual or automatic confirmation """ + + status = "follow_request" activity_serializer = activitypub.Follow def save(self, *args, broadcast=True, **kwargs): - ''' make sure the follow or block relationship doesn't already exist ''' + """ make sure the follow or block relationship doesn't already exist """ # don't create a request if a follow already exists if UserFollows.objects.filter( - user_subject=self.user_subject, - user_object=self.user_object, - ).exists(): + user_subject=self.user_subject, + user_object=self.user_object, + ).exists(): raise IntegrityError() # blocking in either direction is a no-go if UserBlocks.objects.filter( - Q( - user_subject=self.user_subject, - user_object=self.user_object, - ) | Q( - user_subject=self.user_object, - user_object=self.user_subject, - ) - ).exists(): + Q( + user_subject=self.user_subject, + user_object=self.user_object, + ) + | Q( + user_subject=self.user_object, + user_object=self.user_subject, + ) + ).exists(): raise IntegrityError() super().save(*args, **kwargs) @@ -125,39 +130,35 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): if not manually_approves: self.accept() - model = apps.get_model('bookwyrm.Notification', require_ready=True) - notification_type = 'FOLLOW_REQUEST' if \ - manually_approves else 'FOLLOW' + model = apps.get_model("bookwyrm.Notification", require_ready=True) + notification_type = "FOLLOW_REQUEST" if manually_approves else "FOLLOW" model.objects.create( user=self.user_object, related_user=self.user_subject, notification_type=notification_type, ) - def accept(self): - ''' turn this request into the real deal''' + """ turn this request into the real deal""" user = self.user_object if not self.user_subject.local: activity = activitypub.Accept( - id=self.get_remote_id(status='accepts'), + id=self.get_remote_id(status="accepts"), actor=self.user_object.remote_id, - object=self.to_activity() + object=self.to_activity(), ).serialize() self.broadcast(activity, user) with transaction.atomic(): UserFollows.from_request(self) self.delete() - - def reject(self): - ''' generate a Reject for this follow request ''' + """ generate a Reject for this follow request """ if self.user_object.local: activity = activitypub.Reject( - id=self.get_remote_id(status='rejects'), + id=self.get_remote_id(status="rejects"), actor=self.user_object.remote_id, - object=self.to_activity() + object=self.to_activity(), ).serialize() self.broadcast(activity, self.user_object) @@ -165,19 +166,20 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): class UserBlocks(ActivityMixin, UserRelationship): - ''' prevent another user from following you and seeing your posts ''' - status = 'blocks' + """ prevent another user from following you and seeing your posts """ + + status = "blocks" activity_serializer = activitypub.Block def save(self, *args, **kwargs): - ''' remove follow or follow request rels after a block is created ''' + """ remove follow or follow request rels after a block is created """ super().save(*args, **kwargs) UserFollows.objects.filter( - Q(user_subject=self.user_subject, user_object=self.user_object) | \ - Q(user_subject=self.user_object, user_object=self.user_subject) + Q(user_subject=self.user_subject, user_object=self.user_object) + | Q(user_subject=self.user_object, user_object=self.user_subject) ).delete() UserFollowRequest.objects.filter( - Q(user_subject=self.user_subject, user_object=self.user_object) | \ - Q(user_subject=self.user_object, user_object=self.user_subject) + Q(user_subject=self.user_subject, user_object=self.user_object) + | Q(user_subject=self.user_object, user_object=self.user_subject) ).delete() diff --git a/bookwyrm/models/shelf.py b/bookwyrm/models/shelf.py index dfb8b9b31..965541a29 100644 --- a/bookwyrm/models/shelf.py +++ b/bookwyrm/models/shelf.py @@ -1,4 +1,4 @@ -''' puttin' books on shelves ''' +""" puttin' books on shelves """ import re from django.db import models @@ -9,61 +9,68 @@ from . import fields class Shelf(OrderedCollectionMixin, BookWyrmModel): - ''' a list of books owned by a user ''' + """ a list of books owned by a user """ + name = fields.CharField(max_length=100) identifier = models.CharField(max_length=100) user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='owner') + "User", on_delete=models.PROTECT, activitypub_field="owner" + ) editable = models.BooleanField(default=True) privacy = fields.PrivacyField() books = models.ManyToManyField( - 'Edition', + "Edition", symmetrical=False, - through='ShelfBook', - through_fields=('shelf', 'book') + through="ShelfBook", + through_fields=("shelf", "book"), ) activity_serializer = activitypub.Shelf def save(self, *args, **kwargs): - ''' set the identifier ''' + """ set the identifier """ super().save(*args, **kwargs) if not self.identifier: - slug = re.sub(r'[^\w]', '', self.name).lower() - self.identifier = '%s-%d' % (slug, self.id) + slug = re.sub(r"[^\w]", "", self.name).lower() + self.identifier = "%s-%d" % (slug, self.id) super().save(*args, **kwargs) @property def collection_queryset(self): - ''' list of books for this shelf, overrides OrderedCollectionMixin ''' - return self.books.all().order_by('shelfbook') + """ list of books for this shelf, overrides OrderedCollectionMixin """ + return self.books.all().order_by("shelfbook") def get_remote_id(self): - ''' shelf identifier instead of id ''' + """ shelf identifier instead of id """ base_path = self.user.remote_id - return '%s/shelf/%s' % (base_path, self.identifier) + return "%s/shelf/%s" % (base_path, self.identifier) class Meta: - ''' user/shelf unqiueness ''' - unique_together = ('user', 'identifier') + """ user/shelf unqiueness """ + + unique_together = ("user", "identifier") class ShelfBook(CollectionItemMixin, BookWyrmModel): - ''' many to many join table for books and shelves ''' + """ many to many join table for books and shelves """ + book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='object') + "Edition", on_delete=models.PROTECT, activitypub_field="object" + ) shelf = fields.ForeignKey( - 'Shelf', on_delete=models.PROTECT, activitypub_field='target') + "Shelf", on_delete=models.PROTECT, activitypub_field="target" + ) user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='actor') + "User", on_delete=models.PROTECT, activitypub_field="actor" + ) activity_serializer = activitypub.Add - object_field = 'book' - collection_field = 'shelf' - + object_field = "book" + collection_field = "shelf" class Meta: - ''' an opinionated constraint! - you can't put a book on shelf twice ''' - unique_together = ('book', 'shelf') - ordering = ('-created_date',) + """an opinionated constraint! + you can't put a book on shelf twice""" + + unique_together = ("book", "shelf") + ordering = ("-created_date",) diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index d39718b30..7fde6781e 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -1,4 +1,4 @@ -''' the particulars for this instance of BookWyrm ''' +""" the particulars for this instance of BookWyrm """ import base64 import datetime @@ -9,36 +9,31 @@ from django.utils import timezone from bookwyrm.settings import DOMAIN from .user import User + class SiteSettings(models.Model): - ''' customized settings for this instance ''' - name = models.CharField(default='BookWyrm', max_length=100) + """ customized settings for this instance """ + + name = models.CharField(default="BookWyrm", max_length=100) instance_tagline = models.CharField( - max_length=150, default='Social Reading and Reviewing') - instance_description = models.TextField( - default='This instance has no description.') + max_length=150, default="Social Reading and Reviewing" + ) + instance_description = models.TextField(default="This instance has no description.") registration_closed_text = models.TextField( - default='Contact an administrator to get an invite') - code_of_conduct = models.TextField( - default='Add a code of conduct here.') - privacy_policy = models.TextField( - default='Add a privacy policy here.') + default="Contact an administrator to get an invite" + ) + code_of_conduct = models.TextField(default="Add a code of conduct here.") + privacy_policy = models.TextField(default="Add a privacy policy here.") allow_registration = models.BooleanField(default=True) - logo = models.ImageField( - upload_to='logos/', null=True, blank=True - ) - logo_small = models.ImageField( - upload_to='logos/', null=True, blank=True - ) - favicon = models.ImageField( - upload_to='logos/', null=True, blank=True - ) + logo = models.ImageField(upload_to="logos/", null=True, blank=True) + logo_small = models.ImageField(upload_to="logos/", null=True, blank=True) + favicon = models.ImageField(upload_to="logos/", null=True, blank=True) support_link = models.CharField(max_length=255, null=True, blank=True) support_title = models.CharField(max_length=100, null=True, blank=True) admin_email = models.EmailField(max_length=255, null=True, blank=True) @classmethod def get(cls): - ''' gets the site settings db entry or defaults ''' + """ gets the site settings db entry or defaults """ try: return cls.objects.get(id=1) except cls.DoesNotExist: @@ -46,12 +41,15 @@ class SiteSettings(models.Model): default_settings.save() return default_settings + def new_access_code(): - ''' the identifier for a user invite ''' - return base64.b32encode(Random.get_random_bytes(5)).decode('ascii') + """ the identifier for a user invite """ + return base64.b32encode(Random.get_random_bytes(5)).decode("ascii") + class SiteInvite(models.Model): - ''' gives someone access to create an account on the instance ''' + """ gives someone access to create an account on the instance """ + created_date = models.DateTimeField(auto_now_add=True) code = models.CharField(max_length=32, default=new_access_code) expiry = models.DateTimeField(blank=True, null=True) @@ -60,34 +58,35 @@ class SiteInvite(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) def valid(self): - ''' make sure it hasn't expired or been used ''' - return ( - (self.expiry is None or self.expiry > timezone.now()) and - (self.use_limit is None or self.times_used < self.use_limit)) + """ make sure it hasn't expired or been used """ + return (self.expiry is None or self.expiry > timezone.now()) and ( + self.use_limit is None or self.times_used < self.use_limit + ) @property def link(self): - ''' formats the invite link ''' - return 'https://{}/invite/{}'.format(DOMAIN, self.code) + """ formats the invite link """ + return "https://{}/invite/{}".format(DOMAIN, self.code) def get_passowrd_reset_expiry(): - ''' give people a limited time to use the link ''' + """ give people a limited time to use the link """ now = timezone.now() return now + datetime.timedelta(days=1) class PasswordReset(models.Model): - ''' gives someone access to create an account on the instance ''' + """ gives someone access to create an account on the instance """ + code = models.CharField(max_length=32, default=new_access_code) expiry = models.DateTimeField(default=get_passowrd_reset_expiry) user = models.OneToOneField(User, on_delete=models.CASCADE) def valid(self): - ''' make sure it hasn't expired or been used ''' + """ make sure it hasn't expired or been used """ return self.expiry > timezone.now() @property def link(self): - ''' formats the invite link ''' - return 'https://{}/password-reset/{}'.format(DOMAIN, self.code) + """ formats the invite link """ + return "https://{}/password-reset/{}".format(DOMAIN, self.code) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index ba9727f58..c18afd1bf 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -1,4 +1,4 @@ -''' models for storing different kinds of Activities ''' +""" models for storing different kinds of Activities """ from dataclasses import MISSING import re @@ -17,76 +17,81 @@ from . import fields class Status(OrderedCollectionPageMixin, BookWyrmModel): - ''' any post, like a reply to a review, etc ''' + """ any post, like a reply to a review, etc """ + user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='attributedTo') + "User", on_delete=models.PROTECT, activitypub_field="attributedTo" + ) content = fields.HtmlField(blank=True, null=True) - mention_users = fields.TagField('User', related_name='mention_user') - mention_books = fields.TagField('Edition', related_name='mention_book') + mention_users = fields.TagField("User", related_name="mention_user") + mention_books = fields.TagField("Edition", related_name="mention_book") local = models.BooleanField(default=True) content_warning = fields.CharField( - max_length=500, blank=True, null=True, activitypub_field='summary') + max_length=500, blank=True, null=True, activitypub_field="summary" + ) privacy = fields.PrivacyField(max_length=255) sensitive = fields.BooleanField(default=False) # created date is different than publish date because of federated posts published_date = fields.DateTimeField( - default=timezone.now, activitypub_field='published') + default=timezone.now, activitypub_field="published" + ) deleted = models.BooleanField(default=False) deleted_date = models.DateTimeField(blank=True, null=True) favorites = models.ManyToManyField( - 'User', + "User", symmetrical=False, - through='Favorite', - through_fields=('status', 'user'), - related_name='user_favorites' + through="Favorite", + through_fields=("status", "user"), + related_name="user_favorites", ) reply_parent = fields.ForeignKey( - 'self', + "self", null=True, on_delete=models.PROTECT, - activitypub_field='inReplyTo', + activitypub_field="inReplyTo", ) objects = InheritanceManager() activity_serializer = activitypub.Note - serialize_reverse_fields = [('attachments', 'attachment', 'id')] - deserialize_reverse_fields = [('attachments', 'attachment')] - + serialize_reverse_fields = [("attachments", "attachment", "id")] + deserialize_reverse_fields = [("attachments", "attachment")] def save(self, *args, **kwargs): - ''' save and notify ''' + """ save and notify """ super().save(*args, **kwargs) - notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) if self.deleted: notification_model.objects.filter(related_status=self).delete() - if self.reply_parent and self.reply_parent.user != self.user and \ - self.reply_parent.user.local: + if ( + self.reply_parent + and self.reply_parent.user != self.user + and self.reply_parent.user.local + ): notification_model.objects.create( user=self.reply_parent.user, - notification_type='REPLY', + notification_type="REPLY", related_user=self.user, related_status=self, ) for mention_user in self.mention_users.all(): # avoid double-notifying about this status - if not mention_user.local or \ - (self.reply_parent and \ - mention_user == self.reply_parent.user): + if not mention_user.local or ( + self.reply_parent and mention_user == self.reply_parent.user + ): continue notification_model.objects.create( user=mention_user, - notification_type='MENTION', + notification_type="MENTION", related_user=self.user, related_status=self, ) - def delete(self, *args, **kwargs):#pylint: disable=unused-argument - ''' "delete" a status ''' - if hasattr(self, 'boosted_status'): + def delete(self, *args, **kwargs): # pylint: disable=unused-argument + """ "delete" a status """ + if hasattr(self, "boosted_status"): # okay but if it's a boost really delete it super().delete(*args, **kwargs) return @@ -96,141 +101,154 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @property def recipients(self): - ''' tagged users who definitely need to get this status in broadcast ''' + """ tagged users who definitely need to get this status in broadcast """ mentions = [u for u in self.mention_users.all() if not u.local] - if hasattr(self, 'reply_parent') and self.reply_parent \ - and not self.reply_parent.user.local: + if ( + hasattr(self, "reply_parent") + and self.reply_parent + and not self.reply_parent.user.local + ): mentions.append(self.reply_parent.user) return list(set(mentions)) @classmethod def ignore_activity(cls, activity): - ''' keep notes if they are replies to existing statuses ''' - if activity.type == 'Announce': + """ keep notes if they are replies to existing statuses """ + if activity.type == "Announce": # keep it if the booster or the boosted are local boosted = activitypub.resolve_remote_id(activity.object, save=False) return cls.ignore_activity(boosted.to_activity_dataclass()) # keep if it if it's a custom type - if activity.type != 'Note': + if activity.type != "Note": return False - if cls.objects.filter( - remote_id=activity.inReplyTo).exists(): + if cls.objects.filter(remote_id=activity.inReplyTo).exists(): return False # keep notes if they mention local users if activity.tag == MISSING or activity.tag is None: return True - tags = [l['href'] for l in activity.tag if l['type'] == 'Mention'] - user_model = apps.get_model('bookwyrm.User', require_ready=True) + tags = [l["href"] for l in activity.tag if l["type"] == "Mention"] + user_model = apps.get_model("bookwyrm.User", require_ready=True) for tag in tags: - if user_model.objects.filter( - remote_id=tag, local=True).exists(): + if user_model.objects.filter(remote_id=tag, local=True).exists(): # we found a mention of a known use boost return False return True @classmethod def replies(cls, status): - ''' load all replies to a status. idk if there's a better way - to write this so it's just a property ''' - return cls.objects.filter( - reply_parent=status - ).select_subclasses().order_by('published_date') + """load all replies to a status. idk if there's a better way + to write this so it's just a property""" + return ( + cls.objects.filter(reply_parent=status) + .select_subclasses() + .order_by("published_date") + ) @property def status_type(self): - ''' expose the type of status for the ui using activity type ''' + """ expose the type of status for the ui using activity type """ return self.activity_serializer.__name__ @property def boostable(self): - ''' you can't boost dms ''' - return self.privacy in ['unlisted', 'public'] + """ you can't boost dms """ + return self.privacy in ["unlisted", "public"] def to_replies(self, **kwargs): - ''' helper function for loading AP serialized replies to a status ''' + """ helper function for loading AP serialized replies to a status """ return self.to_ordered_collection( self.replies(self), - remote_id='%s/replies' % self.remote_id, + remote_id="%s/replies" % self.remote_id, collection_only=True, **kwargs ).serialize() - def to_activity_dataclass(self, pure=False):# pylint: disable=arguments-differ - ''' return tombstone if the status is deleted ''' + def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ + """ return tombstone if the status is deleted """ if self.deleted: return activitypub.Tombstone( id=self.remote_id, url=self.remote_id, deleted=self.deleted_date.isoformat(), - published=self.deleted_date.isoformat() + published=self.deleted_date.isoformat(), ) activity = ActivitypubMixin.to_activity_dataclass(self) activity.replies = self.to_replies() # "pure" serialization for non-bookwyrm instances - if pure and hasattr(self, 'pure_content'): + if pure and hasattr(self, "pure_content"): activity.content = self.pure_content - if hasattr(activity, 'name'): + if hasattr(activity, "name"): activity.name = self.pure_name activity.type = self.pure_type activity.attachment = [ - image_serializer(b.cover, b.alt_text) \ - for b in self.mention_books.all()[:4] if b.cover] - if hasattr(self, 'book') and self.book.cover: + image_serializer(b.cover, b.alt_text) + for b in self.mention_books.all()[:4] + if b.cover + ] + if hasattr(self, "book") and self.book.cover: activity.attachment.append( image_serializer(self.book.cover, self.book.alt_text) ) return activity - def to_activity(self, pure=False):# pylint: disable=arguments-differ - ''' json serialized activitypub class ''' + def to_activity(self, pure=False): # pylint: disable=arguments-differ + """ json serialized activitypub class """ return self.to_activity_dataclass(pure=pure).serialize() class GeneratedNote(Status): - ''' these are app-generated messages about user activity ''' + """ these are app-generated messages about user activity """ + @property def pure_content(self): - ''' indicate the book in question for mastodon (or w/e) users ''' + """ indicate the book in question for mastodon (or w/e) users """ message = self.content - books = ', '.join( - '"%s"' % (book.remote_id, book.title) \ + books = ", ".join( + '"%s"' % (book.remote_id, book.title) for book in self.mention_books.all() ) - return '%s %s %s' % (self.user.display_name, message, books) + return "%s %s %s" % (self.user.display_name, message, books) activity_serializer = activitypub.GeneratedNote - pure_type = 'Note' + pure_type = "Note" class Comment(Status): - ''' like a review but without a rating and transient ''' + """ like a review but without a rating and transient """ + book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook') + "Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook" + ) @property def pure_content(self): - ''' indicate the book in question for mastodon (or w/e) users ''' - return '%s

(comment on "%s")

' % \ - (self.content, self.book.remote_id, self.book.title) + """ indicate the book in question for mastodon (or w/e) users """ + return '%s

(comment on "%s")

' % ( + self.content, + self.book.remote_id, + self.book.title, + ) activity_serializer = activitypub.Comment - pure_type = 'Note' + pure_type = "Note" class Quotation(Status): - ''' like a review but without a rating and transient ''' + """ like a review but without a rating and transient """ + quote = fields.HtmlField() book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook') + "Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook" + ) @property def pure_content(self): - ''' indicate the book in question for mastodon (or w/e) users ''' - quote = re.sub(r'^

', '

"', self.quote) - quote = re.sub(r'

$', '"

', quote) + """ indicate the book in question for mastodon (or w/e) users """ + quote = re.sub(r"^

", '

"', self.quote) + quote = re.sub(r"

$", '"

', quote) return '%s

-- "%s"

%s' % ( quote, self.book.remote_id, @@ -239,90 +257,86 @@ class Quotation(Status): ) activity_serializer = activitypub.Quotation - pure_type = 'Note' + pure_type = "Note" class Review(Status): - ''' a book review ''' + """ a book review """ + name = fields.CharField(max_length=255, null=True) book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook') + "Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook" + ) rating = fields.IntegerField( default=None, null=True, blank=True, - validators=[MinValueValidator(1), MaxValueValidator(5)] + validators=[MinValueValidator(1), MaxValueValidator(5)], ) @property def pure_name(self): - ''' clarify review names for mastodon serialization ''' + """ clarify review names for mastodon serialization """ if self.rating: - #pylint: disable=bad-string-format-type + # pylint: disable=bad-string-format-type return 'Review of "%s" (%d stars): %s' % ( self.book.title, self.rating, - self.name + self.name, ) - return 'Review of "%s": %s' % ( - self.book.title, - self.name - ) + return 'Review of "%s": %s' % (self.book.title, self.name) @property def pure_content(self): - ''' indicate the book in question for mastodon (or w/e) users ''' + """ indicate the book in question for mastodon (or w/e) users """ return self.content activity_serializer = activitypub.Review - pure_type = 'Article' + pure_type = "Article" class Boost(ActivityMixin, Status): - ''' boost'ing a post ''' + """ boost'ing a post """ + boosted_status = fields.ForeignKey( - 'Status', + "Status", on_delete=models.PROTECT, - related_name='boosters', - activitypub_field='object', + related_name="boosters", + activitypub_field="object", ) activity_serializer = activitypub.Announce def save(self, *args, **kwargs): - ''' save and notify ''' + """ save and notify """ super().save(*args, **kwargs) if not self.boosted_status.user.local: return - notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) notification_model.objects.create( user=self.boosted_status.user, related_status=self.boosted_status, related_user=self.user, - notification_type='BOOST', + notification_type="BOOST", ) def delete(self, *args, **kwargs): - ''' delete and un-notify ''' - notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + """ delete and un-notify """ + notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) notification_model.objects.filter( user=self.boosted_status.user, related_status=self.boosted_status, related_user=self.user, - notification_type='BOOST', + notification_type="BOOST", ).delete() super().delete(*args, **kwargs) - def __init__(self, *args, **kwargs): - ''' the user field is "actor" here instead of "attributedTo" ''' + """ the user field is "actor" here instead of "attributedTo" """ super().__init__(*args, **kwargs) - reserve_fields = ['user', 'boosted_status'] - self.simple_fields = [f for f in self.simple_fields if \ - f.name in reserve_fields] + reserve_fields = ["user", "boosted_status"] + self.simple_fields = [f for f in self.simple_fields if f.name in reserve_fields] self.activity_fields = self.simple_fields self.many_to_many_fields = [] self.image_fields = [] diff --git a/bookwyrm/models/tag.py b/bookwyrm/models/tag.py index 83359170a..2c45b8f91 100644 --- a/bookwyrm/models/tag.py +++ b/bookwyrm/models/tag.py @@ -1,4 +1,4 @@ -''' models for storing different kinds of Activities ''' +""" models for storing different kinds of Activities """ import urllib.parse from django.apps import apps @@ -12,28 +12,30 @@ from . import fields class Tag(OrderedCollectionMixin, BookWyrmModel): - ''' freeform tags for books ''' + """ freeform tags for books """ + name = fields.CharField(max_length=100, unique=True) identifier = models.CharField(max_length=100) @property def books(self): - ''' count of books associated with this tag ''' - edition_model = apps.get_model('bookwyrm.Edition', require_ready=True) - return edition_model.objects.filter( - usertag__tag__identifier=self.identifier - ).order_by('-created_date').distinct() + """ count of books associated with this tag """ + edition_model = apps.get_model("bookwyrm.Edition", require_ready=True) + return ( + edition_model.objects.filter(usertag__tag__identifier=self.identifier) + .order_by("-created_date") + .distinct() + ) collection_queryset = books def get_remote_id(self): - ''' tag should use identifier not id in remote_id ''' - base_path = 'https://%s' % DOMAIN - return '%s/tag/%s' % (base_path, self.identifier) - + """ tag should use identifier not id in remote_id """ + base_path = "https://%s" % DOMAIN + return "%s/tag/%s" % (base_path, self.identifier) def save(self, *args, **kwargs): - ''' create a url-safe lookup key for the tag ''' + """ create a url-safe lookup key for the tag """ if not self.id: # add identifiers to new tags self.identifier = urllib.parse.quote_plus(self.name) @@ -41,18 +43,21 @@ class Tag(OrderedCollectionMixin, BookWyrmModel): class UserTag(CollectionItemMixin, BookWyrmModel): - ''' an instance of a tag on a book by a user ''' + """ an instance of a tag on a book by a user """ + user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='actor') + "User", on_delete=models.PROTECT, activitypub_field="actor" + ) book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='object') - tag = fields.ForeignKey( - 'Tag', on_delete=models.PROTECT, activitypub_field='target') + "Edition", on_delete=models.PROTECT, activitypub_field="object" + ) + tag = fields.ForeignKey("Tag", on_delete=models.PROTECT, activitypub_field="target") activity_serializer = activitypub.Add - object_field = 'book' - collection_field = 'tag' + object_field = "book" + collection_field = "tag" class Meta: - ''' unqiueness constraint ''' - unique_together = ('user', 'book', 'tag') + """ unqiueness constraint """ + + unique_together = ("user", "book", "tag") diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index bbeb10ccb..440b65d3d 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -1,4 +1,4 @@ -''' database schema for user data ''' +""" database schema for user data """ import re from urllib.parse import urlparse @@ -23,25 +23,28 @@ from . import fields, Review class User(OrderedCollectionPageMixin, AbstractUser): - ''' a user who wants to read books ''' + """ a user who wants to read books """ + username = fields.UsernameField() email = models.EmailField(unique=True, null=True) key_pair = fields.OneToOneField( - 'KeyPair', + "KeyPair", on_delete=models.CASCADE, - blank=True, null=True, - activitypub_field='publicKey', - related_name='owner' + blank=True, + null=True, + activitypub_field="publicKey", + related_name="owner", ) inbox = fields.RemoteIdField(unique=True) shared_inbox = fields.RemoteIdField( - activitypub_field='sharedInbox', - activitypub_wrapper='endpoints', + activitypub_field="sharedInbox", + activitypub_wrapper="endpoints", deduplication_field=False, - null=True) + null=True, + ) federated_server = models.ForeignKey( - 'FederatedServer', + "FederatedServer", on_delete=models.PROTECT, null=True, blank=True, @@ -59,54 +62,58 @@ class User(OrderedCollectionPageMixin, AbstractUser): # name is your display name, which you can change at will name = fields.CharField(max_length=100, null=True, blank=True) avatar = fields.ImageField( - upload_to='avatars/', blank=True, null=True, - activitypub_field='icon', alt_field='alt_text') + upload_to="avatars/", + blank=True, + null=True, + activitypub_field="icon", + alt_field="alt_text", + ) followers = fields.ManyToManyField( - 'self', + "self", link_only=True, symmetrical=False, - through='UserFollows', - through_fields=('user_object', 'user_subject'), - related_name='following' + through="UserFollows", + through_fields=("user_object", "user_subject"), + related_name="following", ) follow_requests = models.ManyToManyField( - 'self', + "self", symmetrical=False, - through='UserFollowRequest', - through_fields=('user_subject', 'user_object'), - related_name='follower_requests' + through="UserFollowRequest", + through_fields=("user_subject", "user_object"), + related_name="follower_requests", ) blocks = models.ManyToManyField( - 'self', + "self", symmetrical=False, - through='UserBlocks', - through_fields=('user_subject', 'user_object'), - related_name='blocked_by' + through="UserBlocks", + through_fields=("user_subject", "user_object"), + related_name="blocked_by", ) favorites = models.ManyToManyField( - 'Status', + "Status", symmetrical=False, - through='Favorite', - through_fields=('user', 'status'), - related_name='favorite_statuses' + through="Favorite", + through_fields=("user", "status"), + related_name="favorite_statuses", ) - remote_id = fields.RemoteIdField( - null=True, unique=True, activitypub_field='id') + remote_id = fields.RemoteIdField(null=True, unique=True, activitypub_field="id") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) last_active_date = models.DateTimeField(auto_now=True) manually_approves_followers = fields.BooleanField(default=False) - name_field = 'username' + name_field = "username" + @property def alt_text(self): - ''' alt text with username ''' - return 'avatar for %s' % (self.localname or self.username) + """ alt text with username """ + return "avatar for %s" % (self.localname or self.username) @property def display_name(self): - ''' show the cleanest version of the user's name possible ''' - if self.name and self.name != '': + """ show the cleanest version of the user's name possible """ + if self.name and self.name != "": return self.name return self.localname or self.username @@ -114,78 +121,82 @@ class User(OrderedCollectionPageMixin, AbstractUser): @classmethod def viewer_aware_objects(cls, viewer): - ''' the user queryset filtered for the context of the logged in user ''' + """ the user queryset filtered for the context of the logged in user """ queryset = cls.objects.filter(is_active=True) if viewer.is_authenticated: - queryset = queryset.exclude( - blocks=viewer - ) + queryset = queryset.exclude(blocks=viewer) return queryset def to_outbox(self, filter_type=None, **kwargs): - ''' an ordered collection of statuses ''' + """ an ordered collection of statuses """ if filter_type: filter_class = apps.get_model( - 'bookwyrm.%s' % filter_type, require_ready=True) + "bookwyrm.%s" % filter_type, require_ready=True + ) if not issubclass(filter_class, Status): raise TypeError( - 'filter_status_class must be a subclass of models.Status') + "filter_status_class must be a subclass of models.Status" + ) queryset = filter_class.objects else: queryset = Status.objects - queryset = queryset.filter( - user=self, - deleted=False, - privacy__in=['public', 'unlisted'], - ).select_subclasses().order_by('-published_date') - return self.to_ordered_collection(queryset, \ - collection_only=True, remote_id=self.outbox, **kwargs).serialize() + queryset = ( + queryset.filter( + user=self, + deleted=False, + privacy__in=["public", "unlisted"], + ) + .select_subclasses() + .order_by("-published_date") + ) + return self.to_ordered_collection( + queryset, collection_only=True, remote_id=self.outbox, **kwargs + ).serialize() def to_following_activity(self, **kwargs): - ''' activitypub following list ''' - remote_id = '%s/following' % self.remote_id + """ activitypub following list """ + remote_id = "%s/following" % self.remote_id return self.to_ordered_collection( - self.following.order_by('-updated_date').all(), + self.following.order_by("-updated_date").all(), remote_id=remote_id, id_only=True, **kwargs ) def to_followers_activity(self, **kwargs): - ''' activitypub followers list ''' - remote_id = '%s/followers' % self.remote_id + """ activitypub followers list """ + remote_id = "%s/followers" % self.remote_id return self.to_ordered_collection( - self.followers.order_by('-updated_date').all(), + self.followers.order_by("-updated_date").all(), remote_id=remote_id, id_only=True, **kwargs ) def to_activity(self): - ''' override default AP serializer to add context object - idk if this is the best way to go about this ''' + """override default AP serializer to add context object + idk if this is the best way to go about this""" activity_object = super().to_activity() - activity_object['@context'] = [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + activity_object["@context"] = [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", { - 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers', - 'schema': 'http://schema.org#', - 'PropertyValue': 'schema:PropertyValue', - 'value': 'schema:value', - } + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + }, ] return activity_object - def save(self, *args, **kwargs): - ''' populate fields for new local users ''' + """ populate fields for new local users """ created = not bool(self.id) if not self.local and not re.match(regex.full_username, self.username): # generate a username that uses the domain (webfinger format) actor_parts = urlparse(self.remote_id) - self.username = '%s@%s' % (self.username, actor_parts.netloc) + self.username = "%s@%s" % (self.username, actor_parts.netloc) super().save(*args, **kwargs) # this user already exists, no need to populate fields @@ -200,114 +211,120 @@ class User(OrderedCollectionPageMixin, AbstractUser): return # populate fields for local users - self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.localname) - self.inbox = '%s/inbox' % self.remote_id - self.shared_inbox = 'https://%s/inbox' % DOMAIN - self.outbox = '%s/outbox' % self.remote_id + self.remote_id = "https://%s/user/%s" % (DOMAIN, self.localname) + self.inbox = "%s/inbox" % self.remote_id + self.shared_inbox = "https://%s/inbox" % DOMAIN + self.outbox = "%s/outbox" % self.remote_id # an id needs to be set before we can proceed with related models super().save(*args, **kwargs) # make users editors by default try: - self.groups.add(Group.objects.get(name='editor')) + self.groups.add(Group.objects.get(name="editor")) except Group.DoesNotExist: # this should only happen in tests pass # create keys and shelves for new local users self.key_pair = KeyPair.objects.create( - remote_id='%s/#main-key' % self.remote_id) + remote_id="%s/#main-key" % self.remote_id + ) self.save(broadcast=False) - shelves = [{ - 'name': 'To Read', - 'identifier': 'to-read', - }, { - 'name': 'Currently Reading', - 'identifier': 'reading', - }, { - 'name': 'Read', - 'identifier': 'read', - }] + shelves = [ + { + "name": "To Read", + "identifier": "to-read", + }, + { + "name": "Currently Reading", + "identifier": "reading", + }, + { + "name": "Read", + "identifier": "read", + }, + ] for shelf in shelves: Shelf( - name=shelf['name'], - identifier=shelf['identifier'], + name=shelf["name"], + identifier=shelf["identifier"], user=self, - editable=False + editable=False, ).save(broadcast=False) @property def local_path(self): - ''' this model doesn't inherit bookwyrm model, so here we are ''' - return '/user/%s' % (self.localname or self.username) + """ this model doesn't inherit bookwyrm model, so here we are """ + return "/user/%s" % (self.localname or self.username) class KeyPair(ActivitypubMixin, BookWyrmModel): - ''' public and private keys for a user ''' + """ public and private keys for a user """ + private_key = models.TextField(blank=True, null=True) public_key = fields.TextField( - blank=True, null=True, activitypub_field='publicKeyPem') + blank=True, null=True, activitypub_field="publicKeyPem" + ) activity_serializer = activitypub.PublicKey - serialize_reverse_fields = [('owner', 'owner', 'id')] + serialize_reverse_fields = [("owner", "owner", "id")] def get_remote_id(self): # self.owner is set by the OneToOneField on User - return '%s/#main-key' % self.owner.remote_id + return "%s/#main-key" % self.owner.remote_id def save(self, *args, **kwargs): - ''' create a key pair ''' + """ create a key pair """ # no broadcasting happening here - if 'broadcast' in kwargs: - del kwargs['broadcast'] + if "broadcast" in kwargs: + del kwargs["broadcast"] if not self.public_key: self.private_key, self.public_key = create_key_pair() return super().save(*args, **kwargs) def to_activity(self): - ''' override default AP serializer to add context object - idk if this is the best way to go about this ''' + """override default AP serializer to add context object + idk if this is the best way to go about this""" activity_object = super().to_activity() - del activity_object['@context'] - del activity_object['type'] + del activity_object["@context"] + del activity_object["type"] return activity_object class AnnualGoal(BookWyrmModel): - ''' set a goal for how many books you read in a year ''' - user = models.ForeignKey('User', on_delete=models.PROTECT) - goal = models.IntegerField( - validators=[MinValueValidator(1)] - ) + """ set a goal for how many books you read in a year """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + goal = models.IntegerField(validators=[MinValueValidator(1)]) year = models.IntegerField(default=timezone.now().year) privacy = models.CharField( - max_length=255, - default='public', - choices=fields.PrivacyLevels.choices + max_length=255, default="public", choices=fields.PrivacyLevels.choices ) class Meta: - ''' unqiueness constraint ''' - unique_together = ('user', 'year') + """ unqiueness constraint """ + + unique_together = ("user", "year") def get_remote_id(self): - ''' put the year in the path ''' - return '%s/goal/%d' % (self.user.remote_id, self.year) + """ put the year in the path """ + return "%s/goal/%d" % (self.user.remote_id, self.year) @property def books(self): - ''' the books you've read this year ''' - return self.user.readthrough_set.filter( - finish_date__year__gte=self.year - ).order_by('-finish_date').all() - + """ the books you've read this year """ + return ( + self.user.readthrough_set.filter(finish_date__year__gte=self.year) + .order_by("-finish_date") + .all() + ) @property def ratings(self): - ''' ratings for books read this year ''' + """ ratings for books read this year """ book_ids = [r.book.id for r in self.books] reviews = Review.objects.filter( user=self.user, @@ -315,55 +332,50 @@ class AnnualGoal(BookWyrmModel): ) return {r.book.id: r.rating for r in reviews} - @property def progress_percent(self): - ''' how close to your goal, in percent form ''' + """ how close to your goal, in percent form """ return int(float(self.book_count / self.goal) * 100) - @property def book_count(self): - ''' how many books you've read this year ''' + """ how many books you've read this year """ return self.user.readthrough_set.filter( - finish_date__year__gte=self.year).count() + finish_date__year__gte=self.year + ).count() @app.task def set_remote_server(user_id): - ''' figure out the user's remote server in the background ''' + """ figure out the user's remote server in the background """ user = User.objects.get(id=user_id) actor_parts = urlparse(user.remote_id) - user.federated_server = \ - get_or_create_remote_server(actor_parts.netloc) + user.federated_server = get_or_create_remote_server(actor_parts.netloc) user.save(broadcast=False) if user.bookwyrm_user: get_remote_reviews.delay(user.outbox) def get_or_create_remote_server(domain): - ''' get info on a remote server ''' + """ get info on a remote server """ try: - return FederatedServer.objects.get( - server_name=domain - ) + return FederatedServer.objects.get(server_name=domain) except FederatedServer.DoesNotExist: pass try: - data = get_data('https://%s/.well-known/nodeinfo' % domain) + data = get_data("https://%s/.well-known/nodeinfo" % domain) try: - nodeinfo_url = data.get('links')[0].get('href') + nodeinfo_url = data.get("links")[0].get("href") except (TypeError, KeyError): raise ConnectorException() data = get_data(nodeinfo_url) - application_type = data.get('software', {}).get('name') - application_version = data.get('software', {}).get('version') + application_type = data.get("software", {}).get("name") + application_version = data.get("software", {}).get("version") except ConnectorException: application_type = application_version = None - server = FederatedServer.objects.create( server_name=domain, application_type=application_type, @@ -374,12 +386,12 @@ def get_or_create_remote_server(domain): @app.task def get_remote_reviews(outbox): - ''' ingest reviews by a new remote bookwyrm user ''' - outbox_page = outbox + '?page=true&type=Review' + """ ingest reviews by a new remote bookwyrm user """ + outbox_page = outbox + "?page=true&type=Review" data = get_data(outbox_page) # TODO: pagination? - for activity in data['orderedItems']: - if not activity['type'] == 'Review': + for activity in data["orderedItems"]: + if not activity["type"] == "Review": continue activitypub.Review(**activity).to_model() diff --git a/bookwyrm/sanitize_html.py b/bookwyrm/sanitize_html.py index be7fb56fd..2a630f838 100644 --- a/bookwyrm/sanitize_html.py +++ b/bookwyrm/sanitize_html.py @@ -1,56 +1,63 @@ -''' html parser to clean up incoming text from unknown sources ''' +""" html parser to clean up incoming text from unknown sources """ from html.parser import HTMLParser -class InputHtmlParser(HTMLParser):#pylint: disable=abstract-method - ''' Removes any html that isn't allowed_tagsed from a block ''' + +class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method + """ Removes any html that isn't allowed_tagsed from a block """ def __init__(self): HTMLParser.__init__(self) self.allowed_tags = [ - 'p', 'blockquote', 'br', - 'b', 'i', 'strong', 'em', 'pre', - 'a', 'span', 'ul', 'ol', 'li' + "p", + "blockquote", + "br", + "b", + "i", + "strong", + "em", + "pre", + "a", + "span", + "ul", + "ol", + "li", ] self.tag_stack = [] self.output = [] # if the html appears invalid, we just won't allow any at all self.allow_html = True - def handle_starttag(self, tag, attrs): - ''' check if the tag is valid ''' + """ check if the tag is valid """ if self.allow_html and tag in self.allowed_tags: - self.output.append(('tag', self.get_starttag_text())) + self.output.append(("tag", self.get_starttag_text())) self.tag_stack.append(tag) else: - self.output.append(('data', '')) - + self.output.append(("data", "")) def handle_endtag(self, tag): - ''' keep the close tag ''' + """ keep the close tag """ if not self.allow_html or tag not in self.allowed_tags: - self.output.append(('data', '')) + self.output.append(("data", "")) return if not self.tag_stack or self.tag_stack[-1] != tag: # the end tag doesn't match the most recent start tag self.allow_html = False - self.output.append(('data', '')) + self.output.append(("data", "")) return self.tag_stack = self.tag_stack[:-1] - self.output.append(('tag', '' % tag)) - + self.output.append(("tag", "" % tag)) def handle_data(self, data): - ''' extract the answer, if we're in an answer tag ''' - self.output.append(('data', data)) - + """ extract the answer, if we're in an answer tag """ + self.output.append(("data", data)) def get_output(self): - ''' convert the output from a list of tuples to a string ''' + """ convert the output from a list of tuples to a string """ if self.tag_stack: self.allow_html = False if not self.allow_html: - return ''.join(v for (k, v) in self.output if k == 'data') - return ''.join(v for (k, v) in self.output) + return "".join(v for (k, v) in self.output if k == "data") + return "".join(v for (k, v) in self.output) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 45fdbd9da..bcff58287 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -1,4 +1,4 @@ -''' bookwyrm settings and configuration ''' +""" bookwyrm settings and configuration """ import os from environs import Env @@ -7,129 +7,129 @@ from django.utils.translation import gettext_lazy as _ env = Env() -DOMAIN = env('DOMAIN') -VERSION = '0.0.1' +DOMAIN = env("DOMAIN") +VERSION = "0.0.1" -PAGE_LENGTH = env('PAGE_LENGTH', 15) +PAGE_LENGTH = env("PAGE_LENGTH", 15) # celery -CELERY_BROKER = env('CELERY_BROKER') -CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND') -CELERY_ACCEPT_CONTENT = ['application/json'] -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' +CELERY_BROKER = env("CELERY_BROKER") +CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND") +CELERY_ACCEPT_CONTENT = ["application/json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" # email -EMAIL_HOST = env('EMAIL_HOST') -EMAIL_PORT = env('EMAIL_PORT', 587) -EMAIL_HOST_USER = env('EMAIL_HOST_USER') -EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') -EMAIL_USE_TLS = env('EMAIL_USE_TLS', True) +EMAIL_HOST = env("EMAIL_HOST") +EMAIL_PORT = env("EMAIL_PORT", 587) +EMAIL_HOST_USER = env("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") +EMAIL_USE_TLS = env("EMAIL_USE_TLS", True) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'),] +LOCALE_PATHS = [ + os.path.join(BASE_DIR, "locale"), +] # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = env('SECRET_KEY') +SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool('DEBUG', True) +DEBUG = env.bool("DEBUG", True) -ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', ['*']) -OL_URL = env('OL_URL') +ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"]) +OL_URL = env("OL_URL") # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - 'django_rename_app', - 'bookwyrm', - 'celery', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", + "django_rename_app", + "bookwyrm", + "celery", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'bookwyrm.urls' +ROOT_URLCONF = "bookwyrm.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': ['templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'bookwyrm.context_processors.site_settings', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "bookwyrm.context_processors.site_settings", ], }, }, ] -WSGI_APPLICATION = 'bookwyrm.wsgi.application' +WSGI_APPLICATION = "bookwyrm.wsgi.application" # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases -BOOKWYRM_DATABASE_BACKEND = env('BOOKWYRM_DATABASE_BACKEND', 'postgres') +BOOKWYRM_DATABASE_BACKEND = env("BOOKWYRM_DATABASE_BACKEND", "postgres") BOOKWYRM_DBS = { - 'postgres': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': env('POSTGRES_DB', 'fedireads'), - 'USER': env('POSTGRES_USER', 'fedireads'), - 'PASSWORD': env('POSTGRES_PASSWORD', 'fedireads'), - 'HOST': env('POSTGRES_HOST', ''), - 'PORT': 5432 + "postgres": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": env("POSTGRES_DB", "fedireads"), + "USER": env("POSTGRES_USER", "fedireads"), + "PASSWORD": env("POSTGRES_PASSWORD", "fedireads"), + "HOST": env("POSTGRES_HOST", ""), + "PORT": 5432, }, } -DATABASES = { - 'default': BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND] -} +DATABASES = {"default": BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND]} -LOGIN_URL = '/login/' -AUTH_USER_MODEL = 'bookwyrm.User' +LOGIN_URL = "/login/" +AUTH_USER_MODEL = "bookwyrm.User" # Password validation # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -137,17 +137,17 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" LANGUAGES = [ - ('en-us', _('English')), - ('de-de', _('German')), - ('es', _('Spanish')), - ('fr-fr', _('French')), - ('zh-cn', _('Simplified Chinese')), + ("en-us", _("English")), + ("de-de", _("German")), + ("es", _("Spanish")), + ("fr-fr", _("French")), + ("zh-cn", _("Simplified Chinese")), ] -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -160,10 +160,13 @@ USE_TZ = True # https://docs.djangoproject.com/en/2.0/howto/static-files/ PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, env('STATIC_ROOT', 'static')) -MEDIA_URL = '/images/' -MEDIA_ROOT = os.path.join(BASE_DIR, env('MEDIA_ROOT', 'images')) +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) +MEDIA_URL = "/images/" +MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % ( - requests.utils.default_user_agent(), VERSION, DOMAIN) + requests.utils.default_user_agent(), + VERSION, + DOMAIN, +) diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py index ff2816640..80cbfdc79 100644 --- a/bookwyrm/signatures.py +++ b/bookwyrm/signatures.py @@ -1,4 +1,4 @@ -''' signs activitypub activities ''' +""" signs activitypub activities """ import hashlib from urllib.parse import urlparse import datetime @@ -6,54 +6,56 @@ from base64 import b64encode, b64decode from Crypto import Random from Crypto.PublicKey import RSA -from Crypto.Signature import pkcs1_15 #pylint: disable=no-name-in-module +from Crypto.Signature import pkcs1_15 # pylint: disable=no-name-in-module from Crypto.Hash import SHA256 MAX_SIGNATURE_AGE = 300 + def create_key_pair(): - ''' a new public/private key pair, used for creating new users ''' + """ a new public/private key pair, used for creating new users """ random_generator = Random.new().read key = RSA.generate(1024, random_generator) - private_key = key.export_key().decode('utf8') - public_key = key.publickey().export_key().decode('utf8') + private_key = key.export_key().decode("utf8") + public_key = key.publickey().export_key().decode("utf8") return private_key, public_key def make_signature(sender, destination, date, digest): - ''' uses a private key to sign an outgoing message ''' + """ uses a private key to sign an outgoing message """ inbox_parts = urlparse(destination) signature_headers = [ - '(request-target): post %s' % inbox_parts.path, - 'host: %s' % inbox_parts.netloc, - 'date: %s' % date, - 'digest: %s' % digest, + "(request-target): post %s" % inbox_parts.path, + "host: %s" % inbox_parts.netloc, + "date: %s" % date, + "digest: %s" % digest, ] - message_to_sign = '\n'.join(signature_headers) + message_to_sign = "\n".join(signature_headers) signer = pkcs1_15.new(RSA.import_key(sender.key_pair.private_key)) - signed_message = signer.sign(SHA256.new(message_to_sign.encode('utf8'))) + signed_message = signer.sign(SHA256.new(message_to_sign.encode("utf8"))) signature = { - 'keyId': '%s#main-key' % sender.remote_id, - 'algorithm': 'rsa-sha256', - 'headers': '(request-target) host date digest', - 'signature': b64encode(signed_message).decode('utf8'), + "keyId": "%s#main-key" % sender.remote_id, + "algorithm": "rsa-sha256", + "headers": "(request-target) host date digest", + "signature": b64encode(signed_message).decode("utf8"), } - return ','.join('%s="%s"' % (k, v) for (k, v) in signature.items()) + return ",".join('%s="%s"' % (k, v) for (k, v) in signature.items()) def make_digest(data): - ''' creates a message digest for signing ''' - return 'SHA-256=' + b64encode(hashlib.sha256(data.encode('utf-8'))\ - .digest()).decode('utf-8') + """ creates a message digest for signing """ + return "SHA-256=" + b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode( + "utf-8" + ) def verify_digest(request): - ''' checks if a digest is syntactically valid and matches the message ''' - algorithm, digest = request.headers['digest'].split('=', 1) - if algorithm == 'SHA-256': + """ checks if a digest is syntactically valid and matches the message """ + algorithm, digest = request.headers["digest"].split("=", 1) + if algorithm == "SHA-256": hash_function = hashlib.sha256 - elif algorithm == 'SHA-512': + elif algorithm == "SHA-512": hash_function = hashlib.sha512 else: raise ValueError("Unsupported hash function: {}".format(algorithm)) @@ -62,8 +64,10 @@ def verify_digest(request): if b64decode(digest) != expected: raise ValueError("Invalid HTTP Digest header") + class Signature: - ''' read and validate incoming signatures ''' + """ read and validate incoming signatures """ + def __init__(self, key_id, headers, signature): self.key_id = key_id self.headers = headers @@ -71,42 +75,39 @@ class Signature: @classmethod def parse(cls, request): - ''' extract and parse a signature from an http request ''' + """ extract and parse a signature from an http request """ signature_dict = {} - for pair in request.headers['Signature'].split(','): - k, v = pair.split('=', 1) - v = v.replace('"', '') + for pair in request.headers["Signature"].split(","): + k, v = pair.split("=", 1) + v = v.replace('"', "") signature_dict[k] = v try: - key_id = signature_dict['keyId'] - headers = signature_dict['headers'] - signature = b64decode(signature_dict['signature']) + key_id = signature_dict["keyId"] + headers = signature_dict["headers"] + signature = b64decode(signature_dict["signature"]) except KeyError: - raise ValueError('Invalid auth header') + raise ValueError("Invalid auth header") return cls(key_id, headers, signature) def verify(self, public_key, request): - ''' verify rsa signature ''' - if http_date_age(request.headers['date']) > MAX_SIGNATURE_AGE: - raise ValueError( - "Request too old: %s" % (request.headers['date'],)) + """ verify rsa signature """ + if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE: + raise ValueError("Request too old: %s" % (request.headers["date"],)) public_key = RSA.import_key(public_key) comparison_string = [] - for signed_header_name in self.headers.split(' '): - if signed_header_name == '(request-target)': - comparison_string.append( - '(request-target): post %s' % request.path) + for signed_header_name in self.headers.split(" "): + if signed_header_name == "(request-target)": + comparison_string.append("(request-target): post %s" % request.path) else: - if signed_header_name == 'digest': + if signed_header_name == "digest": verify_digest(request) - comparison_string.append('%s: %s' % ( - signed_header_name, - request.headers[signed_header_name] - )) - comparison_string = '\n'.join(comparison_string) + comparison_string.append( + "%s: %s" % (signed_header_name, request.headers[signed_header_name]) + ) + comparison_string = "\n".join(comparison_string) signer = pkcs1_15.new(public_key) digest = SHA256.new() @@ -117,7 +118,7 @@ class Signature: def http_date_age(datestr): - ''' age of a signature in seconds ''' - parsed = datetime.datetime.strptime(datestr, '%a, %d %b %Y %H:%M:%S GMT') + """ age of a signature in seconds """ + parsed = datetime.datetime.strptime(datestr, "%a, %d %b %Y %H:%M:%S GMT") delta = datetime.datetime.utcnow() - parsed return delta.total_seconds() diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 4dc4991d0..7f0757410 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -1,4 +1,4 @@ -''' Handle user activity ''' +""" Handle user activity """ from django.db import transaction from django.utils import timezone @@ -7,14 +7,14 @@ from bookwyrm.sanitize_html import InputHtmlParser def delete_status(status): - ''' replace the status with a tombstone ''' + """ replace the status with a tombstone """ status.deleted = True status.deleted_date = timezone.now() status.save() -def create_generated_note(user, content, mention_books=None, privacy='public'): - ''' a note created by the app about user activity ''' +def create_generated_note(user, content, mention_books=None, privacy="public"): + """ a note created by the app about user activity """ # sanitize input html parser = InputHtmlParser() parser.feed(content) @@ -22,11 +22,7 @@ def create_generated_note(user, content, mention_books=None, privacy='public'): with transaction.atomic(): # create but don't save - status = models.GeneratedNote( - user=user, - content=content, - privacy=privacy - ) + status = models.GeneratedNote(user=user, content=content, privacy=privacy) # we have to save it to set the related fields, but hold off on telling # folks about it because it is not ready status.save(broadcast=False) diff --git a/bookwyrm/tasks.py b/bookwyrm/tasks.py index fc0b9739b..23765f09b 100644 --- a/bookwyrm/tasks.py +++ b/bookwyrm/tasks.py @@ -1,12 +1,12 @@ -''' background tasks ''' +""" background tasks """ import os from celery import Celery from bookwyrm import settings # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'celerywyrm.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings") app = Celery( - 'tasks', + "tasks", broker=settings.CELERY_BROKER, ) diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index 67354ac65..77be0ca39 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -1,4 +1,4 @@ -''' template filters ''' +""" template filters """ from uuid import uuid4 from datetime import datetime @@ -13,66 +13,78 @@ from bookwyrm.views.status import to_markdown register = template.Library() -@register.filter(name='dict_key') + +@register.filter(name="dict_key") def dict_key(d, k): - ''' Returns the given key from a dictionary. ''' + """ Returns the given key from a dictionary. """ return d.get(k) or 0 -@register.filter(name='rating') +@register.filter(name="rating") def get_rating(book, user): - ''' get the overall rating of a book ''' + """ get the overall rating of a book """ queryset = views.helpers.privacy_filter( - user, models.Review.objects.filter(book=book)) - return queryset.aggregate(Avg('rating'))['rating__avg'] + user, models.Review.objects.filter(book=book) + ) + return queryset.aggregate(Avg("rating"))["rating__avg"] -@register.filter(name='user_rating') +@register.filter(name="user_rating") def get_user_rating(book, user): - ''' get a user's rating of a book ''' - rating = models.Review.objects.filter( - user=user, - book=book, - rating__isnull=False, - ).order_by('-published_date').first() + """ get a user's rating of a book """ + rating = ( + models.Review.objects.filter( + user=user, + book=book, + rating__isnull=False, + ) + .order_by("-published_date") + .first() + ) if rating: return rating.rating return 0 -@register.filter(name='username') +@register.filter(name="username") def get_user_identifier(user): - ''' use localname for local users, username for remote ''' + """ use localname for local users, username for remote """ return user.localname if user.localname else user.username -@register.filter(name='notification_count') +@register.filter(name="notification_count") def get_notification_count(user): - ''' how many UNREAD notifications are there ''' + """ how many UNREAD notifications are there """ return user.notification_set.filter(read=False).count() -@register.filter(name='replies') +@register.filter(name="replies") def get_replies(status): - ''' get all direct replies to a status ''' - #TODO: this limit could cause problems - return models.Status.objects.filter( - reply_parent=status, - deleted=False, - ).select_subclasses().all()[:10] + """ get all direct replies to a status """ + # TODO: this limit could cause problems + return ( + models.Status.objects.filter( + reply_parent=status, + deleted=False, + ) + .select_subclasses() + .all()[:10] + ) -@register.filter(name='parent') +@register.filter(name="parent") def get_parent(status): - ''' get the reply parent for a status ''' - return models.Status.objects.filter( - id=status.reply_parent_id - ).select_subclasses().get() + """ get the reply parent for a status """ + return ( + models.Status.objects.filter(id=status.reply_parent_id) + .select_subclasses() + .get() + ) -@register.filter(name='liked') +@register.filter(name="liked") def get_user_liked(user, status): - ''' did the given user fav a status? ''' + """ did the given user fav a status? """ try: models.Favorite.objects.get(user=user, status=status) return True @@ -80,15 +92,15 @@ def get_user_liked(user, status): return False -@register.filter(name='boosted') +@register.filter(name="boosted") def get_user_boosted(user, status): - ''' did the given user fav a status? ''' - return user.id in status.boosters.all().values_list('user', flat=True) + """ did the given user fav a status? """ + return user.id in status.boosters.all().values_list("user", flat=True) -@register.filter(name='follow_request_exists') +@register.filter(name="follow_request_exists") def follow_request_exists(user, requester): - ''' see if there is a pending follow request for a user ''' + """ see if there is a pending follow request for a user """ try: models.UserFollowRequest.objects.filter( user_subject=requester, @@ -99,129 +111,139 @@ def follow_request_exists(user, requester): return False -@register.filter(name='boosted_status') +@register.filter(name="boosted_status") def get_boosted(boost): - ''' load a boosted status. have to do this or it wont get foregin keys ''' - return models.Status.objects.select_subclasses().filter( - id=boost.boosted_status.id - ).get() + """ load a boosted status. have to do this or it wont get foregin keys """ + return ( + models.Status.objects.select_subclasses() + .filter(id=boost.boosted_status.id) + .get() + ) -@register.filter(name='book_description') +@register.filter(name="book_description") def get_book_description(book): - ''' use the work's text if the book doesn't have it ''' + """ use the work's text if the book doesn't have it """ return book.description or book.parent_work.description -@register.filter(name='uuid') +@register.filter(name="uuid") def get_uuid(identifier): - ''' for avoiding clashing ids when there are many forms ''' - return '%s%s' % (identifier, uuid4()) + """ for avoiding clashing ids when there are many forms """ + return "%s%s" % (identifier, uuid4()) -@register.filter(name='post_date') +@register.filter(name="post_date") def time_since(date): - ''' concise time ago function ''' + """ concise time ago function """ if not isinstance(date, datetime): - return '' + return "" now = timezone.now() delta = now - date if date < (now - relativedelta(weeks=1)): - formatter = '%b %-d' + formatter = "%b %-d" if date.year != now.year: - formatter += ' %Y' + formatter += " %Y" return date.strftime(formatter) delta = relativedelta(now, date) if delta.days: - return '%dd' % delta.days + return "%dd" % delta.days if delta.hours: - return '%dh' % delta.hours + return "%dh" % delta.hours if delta.minutes: - return '%dm' % delta.minutes - return '%ds' % delta.seconds + return "%dm" % delta.minutes + return "%ds" % delta.seconds -@register.filter(name='to_markdown') +@register.filter(name="to_markdown") def get_markdown(content): - ''' convert markdown to html ''' + """ convert markdown to html """ if content: return to_markdown(content) return None -@register.filter(name='mentions') -def get_mentions(status, user): - ''' people to @ in a reply: the parent and all mentions ''' - mentions = set([status.user] + list(status.mention_users.all())) - return ' '.join( - '@' + get_user_identifier(m) for m in mentions if not m == user) + ' ' -@register.filter(name='status_preview_name') +@register.filter(name="mentions") +def get_mentions(status, user): + """ people to @ in a reply: the parent and all mentions """ + mentions = set([status.user] + list(status.mention_users.all())) + return ( + " ".join("@" + get_user_identifier(m) for m in mentions if not m == user) + " " + ) + + +@register.filter(name="status_preview_name") def get_status_preview_name(obj): - ''' text snippet with book context for a status ''' + """ text snippet with book context for a status """ name = obj.__class__.__name__.lower() - if name == 'review': - return '%s of %s' % (name, obj.book.title) - if name == 'comment': - return '%s on %s' % (name, obj.book.title) - if name == 'quotation': - return '%s from %s' % (name, obj.book.title) + if name == "review": + return "%s of %s" % (name, obj.book.title) + if name == "comment": + return "%s on %s" % (name, obj.book.title) + if name == "quotation": + return "%s from %s" % (name, obj.book.title) return name -@register.filter(name='next_shelf') + +@register.filter(name="next_shelf") def get_next_shelf(current_shelf): - ''' shelf you'd use to update reading progress ''' - if current_shelf == 'to-read': - return 'reading' - if current_shelf == 'reading': - return 'read' - if current_shelf == 'read': - return 'read' - return 'to-read' + """ shelf you'd use to update reading progress """ + if current_shelf == "to-read": + return "reading" + if current_shelf == "reading": + return "read" + if current_shelf == "read": + return "read" + return "to-read" + @register.simple_tag(takes_context=False) def related_status(notification): - ''' for notifications ''' + """ for notifications """ if not notification.related_status: return None - if hasattr(notification.related_status, 'quotation'): + if hasattr(notification.related_status, "quotation"): return notification.related_status.quotation - if hasattr(notification.related_status, 'review'): + if hasattr(notification.related_status, "review"): return notification.related_status.review - if hasattr(notification.related_status, 'comment'): + if hasattr(notification.related_status, "comment"): return notification.related_status.comment return notification.related_status + @register.simple_tag(takes_context=True) def active_shelf(context, book): - ''' check what shelf a user has a book on, if any ''' + """ check what shelf a user has a book on, if any """ shelf = models.ShelfBook.objects.filter( - shelf__user=context['request'].user, - book__in=book.parent_work.editions.all() + shelf__user=context["request"].user, book__in=book.parent_work.editions.all() ).first() - return shelf if shelf else {'book': book} + return shelf if shelf else {"book": book} @register.simple_tag(takes_context=False) def latest_read_through(book, user): - ''' the most recent read activity ''' - return models.ReadThrough.objects.filter( - user=user, - book=book - ).order_by('-start_date').first() + """ the most recent read activity """ + return ( + models.ReadThrough.objects.filter(user=user, book=book) + .order_by("-start_date") + .first() + ) @register.simple_tag(takes_context=False) def active_read_through(book, user): - ''' the most recent read activity ''' - return models.ReadThrough.objects.filter( - user=user, - book=book, - finish_date__isnull=True - ).order_by('-start_date').first() + """ the most recent read activity """ + return ( + models.ReadThrough.objects.filter( + user=user, book=book, finish_date__isnull=True + ) + .order_by("-start_date") + .first() + ) @register.simple_tag(takes_context=False) def comparison_bool(str1, str2): - ''' idk why I need to write a tag for this, it reutrns a bool ''' + """ idk why I need to write a tag for this, it reutrns a bool """ return str1 == str2 diff --git a/bookwyrm/tests/activitypub/test_author.py b/bookwyrm/tests/activitypub/test_author.py index fd31f105e..e65f86b76 100644 --- a/bookwyrm/tests/activitypub/test_author.py +++ b/bookwyrm/tests/activitypub/test_author.py @@ -7,19 +7,18 @@ from bookwyrm import models class Author(TestCase): def setUp(self): self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', + title="Example Edition", + remote_id="https://example.com/book/1", ) self.author = models.Author.objects.create( - name='Author fullname', - aliases=['One', 'Two'], - bio='bio bio bio', + name="Author fullname", + aliases=["One", "Two"], + bio="bio bio bio", ) - def test_serialize_model(self): activity = self.author.to_activity() - self.assertEqual(activity['id'], self.author.remote_id) - self.assertIsInstance(activity['aliases'], list) - self.assertEqual(activity['aliases'], ['One', 'Two']) - self.assertEqual(activity['name'], 'Author fullname') + self.assertEqual(activity["id"], self.author.remote_id) + self.assertIsInstance(activity["aliases"], list) + self.assertEqual(activity["aliases"], ["One", "Two"]) + self.assertEqual(activity["name"], "Author fullname") diff --git a/bookwyrm/tests/activitypub/test_base_activity.py b/bookwyrm/tests/activitypub/test_base_activity.py index de108eaeb..b3e282619 100644 --- a/bookwyrm/tests/activitypub/test_base_activity.py +++ b/bookwyrm/tests/activitypub/test_base_activity.py @@ -1,4 +1,4 @@ -''' tests the base functionality for activitypub dataclasses ''' +""" tests the base functionality for activitypub dataclasses """ from io import BytesIO import json import pathlib @@ -10,231 +10,229 @@ from PIL import Image import responses from bookwyrm import activitypub -from bookwyrm.activitypub.base_activity import ActivityObject, \ - resolve_remote_id, set_related_field +from bookwyrm.activitypub.base_activity import ( + ActivityObject, + resolve_remote_id, + set_related_field, +) from bookwyrm.activitypub import ActivitySerializerError from bookwyrm import models + class BaseActivity(TestCase): - ''' the super class for model-linked activitypub dataclasses ''' + """ the super class for model-linked activitypub dataclasses """ + def setUp(self): - ''' we're probably going to re-use this so why copy/paste ''' + """ we're probably going to re-use this so why copy/paste """ self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') - self.user.remote_id = 'http://example.com/a/b' + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" + ) + self.user.remote_id = "http://example.com/a/b" self.user.save(broadcast=False) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_user.json' - ) + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") self.userdata = json.loads(datafile.read_bytes()) # don't try to load the user icon - del self.userdata['icon'] + del self.userdata["icon"] image_file = pathlib.Path(__file__).parent.joinpath( - '../../static/images/default_avi.jpg') + "../../static/images/default_avi.jpg" + ) image = Image.open(image_file) output = BytesIO() image.save(output, format=image.format) self.image_data = output.getvalue() def test_init(self): - ''' simple successfuly init ''' - instance = ActivityObject(id='a', type='b') - self.assertTrue(hasattr(instance, 'id')) - self.assertTrue(hasattr(instance, 'type')) + """ simple successfuly init """ + instance = ActivityObject(id="a", type="b") + self.assertTrue(hasattr(instance, "id")) + self.assertTrue(hasattr(instance, "type")) def test_init_missing(self): - ''' init with missing required params ''' + """ init with missing required params """ with self.assertRaises(ActivitySerializerError): ActivityObject() def test_init_extra_fields(self): - ''' init ignoring additional fields ''' - instance = ActivityObject(id='a', type='b', fish='c') - self.assertTrue(hasattr(instance, 'id')) - self.assertTrue(hasattr(instance, 'type')) + """ init ignoring additional fields """ + instance = ActivityObject(id="a", type="b", fish="c") + self.assertTrue(hasattr(instance, "id")) + self.assertTrue(hasattr(instance, "type")) def test_init_default_field(self): - ''' replace an existing required field with a default field ''' + """ replace an existing required field with a default field """ + @dataclass(init=False) class TestClass(ActivityObject): - ''' test class with default field ''' - type: str = 'TestObject' + """ test class with default field """ - instance = TestClass(id='a') - self.assertEqual(instance.id, 'a') - self.assertEqual(instance.type, 'TestObject') + type: str = "TestObject" + + instance = TestClass(id="a") + self.assertEqual(instance.id, "a") + self.assertEqual(instance.type, "TestObject") def test_serialize(self): - ''' simple function for converting dataclass to dict ''' - instance = ActivityObject(id='a', type='b') + """ simple function for converting dataclass to dict """ + instance = ActivityObject(id="a", type="b") serialized = instance.serialize() self.assertIsInstance(serialized, dict) - self.assertEqual(serialized['id'], 'a') - self.assertEqual(serialized['type'], 'b') + self.assertEqual(serialized["id"], "a") + self.assertEqual(serialized["type"], "b") @responses.activate def test_resolve_remote_id(self): - ''' look up or load remote data ''' + """ look up or load remote data """ # existing item - result = resolve_remote_id('http://example.com/a/b', model=models.User) + result = resolve_remote_id("http://example.com/a/b", model=models.User) self.assertEqual(result, self.user) # remote item responses.add( responses.GET, - 'https://example.com/user/mouse', + "https://example.com/user/mouse", json=self.userdata, - status=200) + status=200, + ) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): result = resolve_remote_id( - 'https://example.com/user/mouse', model=models.User) + "https://example.com/user/mouse", model=models.User + ) self.assertIsInstance(result, models.User) - self.assertEqual(result.remote_id, 'https://example.com/user/mouse') - self.assertEqual(result.name, 'MOUSE?? MOUSE!!') + self.assertEqual(result.remote_id, "https://example.com/user/mouse") + self.assertEqual(result.name, "MOUSE?? MOUSE!!") def test_to_model_invalid_model(self): - ''' catch mismatch between activity type and model type ''' - instance = ActivityObject(id='a', type='b') + """ catch mismatch between activity type and model type """ + instance = ActivityObject(id="a", type="b") with self.assertRaises(ActivitySerializerError): instance.to_model(model=models.User) - @responses.activate def test_to_model_image(self): - ''' update an image field ''' + """ update an image field """ activity = activitypub.Person( id=self.user.remote_id, - name='New Name', - preferredUsername='mouse', - inbox='http://www.com/', - outbox='http://www.com/', - followers='', - summary='', - publicKey={ - 'id': 'hi', - 'owner': self.user.remote_id, - 'publicKeyPem': 'hi'}, + name="New Name", + preferredUsername="mouse", + inbox="http://www.com/", + outbox="http://www.com/", + followers="", + summary="", + publicKey={"id": "hi", "owner": self.user.remote_id, "publicKeyPem": "hi"}, endpoints={}, - icon={ - 'type': 'Image', - 'url': 'http://www.example.com/image.jpg' - } + icon={"type": "Image", "url": "http://www.example.com/image.jpg"}, ) responses.add( responses.GET, - 'http://www.example.com/image.jpg', + "http://www.example.com/image.jpg", body=self.image_data, - status=200) + status=200, + ) self.assertIsNone(self.user.avatar.name) with self.assertRaises(ValueError): - self.user.avatar.file #pylint: disable=pointless-statement + self.user.avatar.file # pylint: disable=pointless-statement # this would trigger a broadcast because it's a local user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): activity.to_model(model=models.User, instance=self.user) self.assertIsNotNone(self.user.avatar.file) - self.assertEqual(self.user.name, 'New Name') - self.assertEqual(self.user.key_pair.public_key, 'hi') + self.assertEqual(self.user.name, "New Name") + self.assertEqual(self.user.key_pair.public_key, "hi") def test_to_model_many_to_many(self): - ''' annoying that these all need special handling ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ annoying that these all need special handling """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( - content='test status', + content="test status", user=self.user, ) book = models.Edition.objects.create( - title='Test Edition', remote_id='http://book.com/book') + title="Test Edition", remote_id="http://book.com/book" + ) update_data = activitypub.Note( id=status.remote_id, content=status.content, attributedTo=self.user.remote_id, - published='hi', + published="hi", to=[], cc=[], tag=[ + {"type": "Mention", "name": "gerald", "href": "http://example.com/a/b"}, { - 'type': 'Mention', - 'name': 'gerald', - 'href': 'http://example.com/a/b' + "type": "Edition", + "name": "gerald j. books", + "href": "http://book.com/book", }, - { - 'type': 'Edition', - 'name': 'gerald j. books', - 'href': 'http://book.com/book' - }, - ] + ], ) update_data.to_model(model=models.Status, instance=status) self.assertEqual(status.mention_users.first(), self.user) self.assertEqual(status.mention_books.first(), book) - @responses.activate def test_to_model_one_to_many(self): - ''' these are reversed relationships, where the secondary object - keys the primary object but not vice versa ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """these are reversed relationships, where the secondary object + keys the primary object but not vice versa""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( - content='test status', + content="test status", user=self.user, ) update_data = activitypub.Note( id=status.remote_id, content=status.content, attributedTo=self.user.remote_id, - published='hi', + published="hi", to=[], cc=[], - attachment=[{ - 'url': 'http://www.example.com/image.jpg', - 'name': 'alt text', - 'type': 'Image', - }], + attachment=[ + { + "url": "http://www.example.com/image.jpg", + "name": "alt text", + "type": "Image", + } + ], ) responses.add( responses.GET, - 'http://www.example.com/image.jpg', + "http://www.example.com/image.jpg", body=self.image_data, - status=200) + status=200, + ) # sets the celery task call to the function call - with patch( - 'bookwyrm.activitypub.base_activity.set_related_field.delay'): - with patch('bookwyrm.models.status.Status.ignore_activity') \ - as discarder: + with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"): + with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: discarder.return_value = False update_data.to_model(model=models.Status, instance=status) self.assertIsNone(status.attachments.first()) - @responses.activate def test_set_related_field(self): - ''' celery task to add back-references to created objects ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ celery task to add back-references to created objects """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( - content='test status', + content="test status", user=self.user, ) data = { - 'url': 'http://www.example.com/image.jpg', - 'name': 'alt text', - 'type': 'Image', + "url": "http://www.example.com/image.jpg", + "name": "alt text", + "type": "Image", } responses.add( responses.GET, - 'http://www.example.com/image.jpg', + "http://www.example.com/image.jpg", body=self.image_data, - status=200) - set_related_field( - 'Image', 'Status', 'status', status.remote_id, data) + status=200, + ) + set_related_field("Image", "Status", "status", status.remote_id, data) self.assertIsInstance(status.attachments.first(), models.Image) self.assertIsNotNone(status.attachments.first().image) diff --git a/bookwyrm/tests/activitypub/test_person.py b/bookwyrm/tests/activitypub/test_person.py index 062402813..67aaf891e 100644 --- a/bookwyrm/tests/activitypub/test_person.py +++ b/bookwyrm/tests/activitypub/test_person.py @@ -9,23 +9,19 @@ from bookwyrm import activitypub, models class Person(TestCase): def setUp(self): - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_user.json' - ) + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") self.user_data = json.loads(datafile.read_bytes()) - def test_load_user_data(self): activity = activitypub.Person(**self.user_data) - self.assertEqual(activity.id, 'https://example.com/user/mouse') - self.assertEqual(activity.preferredUsername, 'mouse') - self.assertEqual(activity.type, 'Person') - + self.assertEqual(activity.id, "https://example.com/user/mouse") + self.assertEqual(activity.preferredUsername, "mouse") + self.assertEqual(activity.type, "Person") def test_user_to_model(self): activity = activitypub.Person(**self.user_data) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): user = activity.to_model(model=models.User) - self.assertEqual(user.username, 'mouse@example.com') - self.assertEqual(user.remote_id, 'https://example.com/user/mouse') + self.assertEqual(user.username, "mouse@example.com") + self.assertEqual(user.remote_id, "https://example.com/user/mouse") self.assertFalse(user.local) diff --git a/bookwyrm/tests/activitypub/test_quotation.py b/bookwyrm/tests/activitypub/test_quotation.py index 1cd1f05d4..1f429dd25 100644 --- a/bookwyrm/tests/activitypub/test_quotation.py +++ b/bookwyrm/tests/activitypub/test_quotation.py @@ -1,4 +1,4 @@ -''' quotation activty object serializer class ''' +""" quotation activty object serializer class """ import json import pathlib from unittest.mock import patch @@ -8,43 +8,40 @@ from bookwyrm import activitypub, models class Quotation(TestCase): - ''' we have hecka ways to create statuses ''' + """ we have hecka ways to create statuses """ + def setUp(self): - ''' model objects we'll need ''' - with patch('bookwyrm.models.user.set_remote_server.delay'): + """ model objects we'll need """ + with patch("bookwyrm.models.user.set_remote_server.delay"): self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', + "mouse", + "mouse@mouse.mouse", + "mouseword", local=False, - inbox='https://example.com/user/mouse/inbox', - outbox='https://example.com/user/mouse/outbox', - remote_id='https://example.com/user/mouse', + inbox="https://example.com/user/mouse/inbox", + outbox="https://example.com/user/mouse/outbox", + remote_id="https://example.com/user/mouse", ) self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - ) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_quotation.json' + title="Example Edition", + remote_id="https://example.com/book/1", ) + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_quotation.json") self.status_data = json.loads(datafile.read_bytes()) - def test_quotation_activity(self): - ''' create a Quoteation ap object from json ''' + """ create a Quoteation ap object from json """ quotation = activitypub.Quotation(**self.status_data) - self.assertEqual(quotation.type, 'Quotation') - self.assertEqual( - quotation.id, 'https://example.com/user/mouse/quotation/13') - self.assertEqual(quotation.content, 'commentary') - self.assertEqual(quotation.quote, 'quote body') - self.assertEqual(quotation.inReplyToBook, 'https://example.com/book/1') - self.assertEqual( - quotation.published, '2020-05-10T02:38:31.150343+00:00') - + self.assertEqual(quotation.type, "Quotation") + self.assertEqual(quotation.id, "https://example.com/user/mouse/quotation/13") + self.assertEqual(quotation.content, "commentary") + self.assertEqual(quotation.quote, "quote body") + self.assertEqual(quotation.inReplyToBook, "https://example.com/book/1") + self.assertEqual(quotation.published, "2020-05-10T02:38:31.150343+00:00") def test_activity_to_model(self): - ''' create a model instance from an activity object ''' + """ create a model instance from an activity object """ activity = activitypub.Quotation(**self.status_data) quotation = activity.to_model(model=models.Quotation) diff --git a/bookwyrm/tests/connectors/test_abstract_connector.py b/bookwyrm/tests/connectors/test_abstract_connector.py index 1b3821040..9aa78f6a2 100644 --- a/bookwyrm/tests/connectors/test_abstract_connector.py +++ b/bookwyrm/tests/connectors/test_abstract_connector.py @@ -1,4 +1,4 @@ -''' testing book data connectors ''' +""" testing book data connectors """ from unittest.mock import patch from django.test import TestCase import responses @@ -10,109 +10,115 @@ from bookwyrm.settings import DOMAIN class AbstractConnector(TestCase): - ''' generic code for connecting to outside data sources ''' + """ generic code for connecting to outside data sources """ + def setUp(self): - ''' we need an example connector ''' + """ we need an example connector """ self.connector_info = models.Connector.objects.create( - identifier='example.com', - connector_file='openlibrary', - base_url='https://example.com', - books_url='https://example.com/books', - covers_url='https://example.com/covers', - search_url='https://example.com/search?q=', + identifier="example.com", + connector_file="openlibrary", + base_url="https://example.com", + books_url="https://example.com/books", + covers_url="https://example.com/covers", + search_url="https://example.com/search?q=", ) work_data = { - 'id': 'abc1', - 'title': 'Test work', - 'type': 'work', - 'openlibraryKey': 'OL1234W', + "id": "abc1", + "title": "Test work", + "type": "work", + "openlibraryKey": "OL1234W", } self.work_data = work_data edition_data = { - 'id': 'abc2', - 'title': 'Test edition', - 'type': 'edition', - 'openlibraryKey': 'OL1234M', + "id": "abc2", + "title": "Test edition", + "type": "edition", + "openlibraryKey": "OL1234M", } self.edition_data = edition_data class TestConnector(abstract_connector.AbstractConnector): - ''' nothing added here ''' + """ nothing added here """ + def format_search_result(self, search_result): return search_result + def parse_search_data(self, data): return data + def format_isbn_search_result(self, search_result): return search_result + def parse_isbn_search_data(self, data): return data + def is_work_data(self, data): - return data['type'] == 'work' + return data["type"] == "work" + def get_edition_from_work_data(self, data): return edition_data + def get_work_from_edition_data(self, data): return work_data + def get_authors_from_data(self, data): return [] + def expand_book_data(self, book): pass - self.connector = TestConnector('example.com') + + self.connector = TestConnector("example.com") self.connector.book_mappings = [ - Mapping('id'), - Mapping('title'), - Mapping('openlibraryKey'), + Mapping("id"), + Mapping("title"), + Mapping("openlibraryKey"), ] self.book = models.Edition.objects.create( - title='Test Book', remote_id='https://example.com/book/1234', - openlibrary_key='OL1234M') - + title="Test Book", + remote_id="https://example.com/book/1234", + openlibrary_key="OL1234M", + ) def test_abstract_connector_init(self): - ''' barebones connector for search with defaults ''' + """ barebones connector for search with defaults """ self.assertIsInstance(self.connector.book_mappings, list) - def test_is_available(self): - ''' this isn't used.... ''' + """ this isn't used.... """ self.assertTrue(self.connector.is_available()) self.connector.max_query_count = 1 self.connector.connector.query_count = 2 self.assertFalse(self.connector.is_available()) - def test_get_or_create_book_existing(self): - ''' find an existing book by remote/origin id ''' + """ find an existing book by remote/origin id """ self.assertEqual(models.Book.objects.count(), 1) self.assertEqual( - self.book.remote_id, 'https://%s/book/%d' % (DOMAIN, self.book.id)) - self.assertEqual( - self.book.origin_id, 'https://example.com/book/1234') + self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id) + ) + self.assertEqual(self.book.origin_id, "https://example.com/book/1234") # dedupe by origin id - result = self.connector.get_or_create_book( - 'https://example.com/book/1234') + result = self.connector.get_or_create_book("https://example.com/book/1234") self.assertEqual(models.Book.objects.count(), 1) self.assertEqual(result, self.book) # dedupe by remote id result = self.connector.get_or_create_book( - 'https://%s/book/%d' % (DOMAIN, self.book.id)) + "https://%s/book/%d" % (DOMAIN, self.book.id) + ) self.assertEqual(models.Book.objects.count(), 1) self.assertEqual(result, self.book) @responses.activate def test_get_or_create_book_deduped(self): - ''' load remote data and deduplicate ''' + """ load remote data and deduplicate """ responses.add( - responses.GET, - 'https://example.com/book/abcd', - json=self.edition_data + responses.GET, "https://example.com/book/abcd", json=self.edition_data ) - with patch( - 'bookwyrm.connectors.abstract_connector.load_more_data.delay'): - result = self.connector.get_or_create_book( - 'https://example.com/book/abcd') + with patch("bookwyrm.connectors.abstract_connector.load_more_data.delay"): + result = self.connector.get_or_create_book("https://example.com/book/abcd") self.assertEqual(result, self.book) self.assertEqual(models.Edition.objects.count(), 1) self.assertEqual(models.Edition.objects.count(), 1) diff --git a/bookwyrm/tests/connectors/test_abstract_minimal_connector.py b/bookwyrm/tests/connectors/test_abstract_minimal_connector.py index 9b939067b..957d32334 100644 --- a/bookwyrm/tests/connectors/test_abstract_minimal_connector.py +++ b/bookwyrm/tests/connectors/test_abstract_minimal_connector.py @@ -1,4 +1,4 @@ -''' testing book data connectors ''' +""" testing book data connectors """ from django.test import TestCase import responses @@ -8,99 +8,101 @@ from bookwyrm.connectors.abstract_connector import Mapping, SearchResult class AbstractConnector(TestCase): - ''' generic code for connecting to outside data sources ''' + """ generic code for connecting to outside data sources """ + def setUp(self): - ''' we need an example connector ''' + """ we need an example connector """ self.connector_info = models.Connector.objects.create( - identifier='example.com', - connector_file='openlibrary', - base_url='https://example.com', - books_url='https://example.com/books', - covers_url='https://example.com/covers', - search_url='https://example.com/search?q=', - isbn_search_url='https://example.com/isbn', + identifier="example.com", + connector_file="openlibrary", + base_url="https://example.com", + books_url="https://example.com/books", + covers_url="https://example.com/covers", + search_url="https://example.com/search?q=", + isbn_search_url="https://example.com/isbn", ) class TestConnector(abstract_connector.AbstractMinimalConnector): - ''' nothing added here ''' + """ nothing added here """ + def format_search_result(self, search_result): return search_result + def get_or_create_book(self, remote_id): pass + def parse_search_data(self, data): return data + def format_isbn_search_result(self, search_result): return search_result + def parse_isbn_search_data(self, data): return data - self.test_connector = TestConnector('example.com') + self.test_connector = TestConnector("example.com") def test_abstract_minimal_connector_init(self): - ''' barebones connector for search with defaults ''' + """ barebones connector for search with defaults """ connector = self.test_connector self.assertEqual(connector.connector, self.connector_info) - self.assertEqual(connector.base_url, 'https://example.com') - self.assertEqual(connector.books_url, 'https://example.com/books') - self.assertEqual(connector.covers_url, 'https://example.com/covers') - self.assertEqual(connector.search_url, 'https://example.com/search?q=') - self.assertEqual(connector.isbn_search_url, 'https://example.com/isbn') + self.assertEqual(connector.base_url, "https://example.com") + self.assertEqual(connector.books_url, "https://example.com/books") + self.assertEqual(connector.covers_url, "https://example.com/covers") + self.assertEqual(connector.search_url, "https://example.com/search?q=") + self.assertEqual(connector.isbn_search_url, "https://example.com/isbn") self.assertIsNone(connector.name) - self.assertEqual(connector.identifier, 'example.com') + self.assertEqual(connector.identifier, "example.com") self.assertIsNone(connector.max_query_count) self.assertFalse(connector.local) - @responses.activate def test_search(self): - ''' makes an http request to the outside service ''' + """ makes an http request to the outside service """ responses.add( responses.GET, - 'https://example.com/search?q=a%20book%20title', - json=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'], - status=200) - results = self.test_connector.search('a book title') + "https://example.com/search?q=a%20book%20title", + json=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"], + status=200, + ) + results = self.test_connector.search("a book title") self.assertEqual(len(results), 10) - self.assertEqual(results[0], 'a') - self.assertEqual(results[1], 'b') - self.assertEqual(results[2], 'c') - + self.assertEqual(results[0], "a") + self.assertEqual(results[1], "b") + self.assertEqual(results[2], "c") def test_search_result(self): - ''' a class that stores info about a search result ''' + """ a class that stores info about a search result """ result = SearchResult( - title='Title', - key='https://example.com/book/1', - author='Author Name', - year='1850', + title="Title", + key="https://example.com/book/1", + author="Author Name", + year="1850", connector=self.test_connector, ) # there's really not much to test here, it's just a dataclass self.assertEqual(result.confidence, 1) - self.assertEqual(result.title, 'Title') - + self.assertEqual(result.title, "Title") def test_create_mapping(self): - ''' maps remote fields for book data to bookwyrm activitypub fields ''' - mapping = Mapping('isbn') - self.assertEqual(mapping.local_field, 'isbn') - self.assertEqual(mapping.remote_field, 'isbn') - self.assertEqual(mapping.formatter('bb'), 'bb') - + """ maps remote fields for book data to bookwyrm activitypub fields """ + mapping = Mapping("isbn") + self.assertEqual(mapping.local_field, "isbn") + self.assertEqual(mapping.remote_field, "isbn") + self.assertEqual(mapping.formatter("bb"), "bb") def test_create_mapping_with_remote(self): - ''' the remote field is different than the local field ''' - mapping = Mapping('isbn', remote_field='isbn13') - self.assertEqual(mapping.local_field, 'isbn') - self.assertEqual(mapping.remote_field, 'isbn13') - self.assertEqual(mapping.formatter('bb'), 'bb') - + """ the remote field is different than the local field """ + mapping = Mapping("isbn", remote_field="isbn13") + self.assertEqual(mapping.local_field, "isbn") + self.assertEqual(mapping.remote_field, "isbn13") + self.assertEqual(mapping.formatter("bb"), "bb") def test_create_mapping_with_formatter(self): - ''' a function is provided to modify the data ''' - formatter = lambda x: 'aa' + x - mapping = Mapping('isbn', formatter=formatter) - self.assertEqual(mapping.local_field, 'isbn') - self.assertEqual(mapping.remote_field, 'isbn') + """ a function is provided to modify the data """ + formatter = lambda x: "aa" + x + mapping = Mapping("isbn", formatter=formatter) + self.assertEqual(mapping.local_field, "isbn") + self.assertEqual(mapping.remote_field, "isbn") self.assertEqual(mapping.formatter, formatter) - self.assertEqual(mapping.formatter('bb'), 'aabb') + self.assertEqual(mapping.formatter("bb"), "aabb") diff --git a/bookwyrm/tests/connectors/test_bookwyrm_connector.py b/bookwyrm/tests/connectors/test_bookwyrm_connector.py index 6b00b0e3a..1fc71688d 100644 --- a/bookwyrm/tests/connectors/test_bookwyrm_connector.py +++ b/bookwyrm/tests/connectors/test_bookwyrm_connector.py @@ -1,4 +1,4 @@ -''' testing book data connectors ''' +""" testing book data connectors """ import json import pathlib from django.test import TestCase @@ -9,39 +9,36 @@ from bookwyrm.connectors.abstract_connector import SearchResult class BookWyrmConnector(TestCase): - ''' this connector doesn't do much, just search ''' - def setUp(self): - ''' create the connector ''' - models.Connector.objects.create( - identifier='example.com', - connector_file='bookwyrm_connector', - base_url='https://example.com', - books_url='https://example.com', - covers_url='https://example.com/images/covers', - search_url='https://example.com/search?q=', - ) - self.connector = Connector('example.com') + """ this connector doesn't do much, just search """ - work_file = pathlib.Path(__file__).parent.joinpath( - '../data/bw_work.json') - edition_file = pathlib.Path(__file__).parent.joinpath( - '../data/bw_edition.json') + def setUp(self): + """ create the connector """ + models.Connector.objects.create( + identifier="example.com", + connector_file="bookwyrm_connector", + base_url="https://example.com", + books_url="https://example.com", + covers_url="https://example.com/images/covers", + search_url="https://example.com/search?q=", + ) + self.connector = Connector("example.com") + + work_file = pathlib.Path(__file__).parent.joinpath("../data/bw_work.json") + edition_file = pathlib.Path(__file__).parent.joinpath("../data/bw_edition.json") self.work_data = json.loads(work_file.read_bytes()) self.edition_data = json.loads(edition_file.read_bytes()) - def test_format_search_result(self): - ''' create a SearchResult object from search response json ''' - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/bw_search.json') + """ create a SearchResult object from search response json """ + datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_search_data(search_data) self.assertIsInstance(results, list) result = self.connector.format_search_result(results[0]) self.assertIsInstance(result, SearchResult) - self.assertEqual(result.title, 'Jonathan Strange and Mr Norrell') - self.assertEqual(result.key, 'https://example.com/book/122') - self.assertEqual(result.author, 'Susanna Clarke') + self.assertEqual(result.title, "Jonathan Strange and Mr Norrell") + self.assertEqual(result.key, "https://example.com/book/122") + self.assertEqual(result.author, "Susanna Clarke") self.assertEqual(result.year, 2017) self.assertEqual(result.connector, self.connector) diff --git a/bookwyrm/tests/connectors/test_connector_manager.py b/bookwyrm/tests/connectors/test_connector_manager.py index 783b5a276..4410e011c 100644 --- a/bookwyrm/tests/connectors/test_connector_manager.py +++ b/bookwyrm/tests/connectors/test_connector_manager.py @@ -1,54 +1,49 @@ -''' interface between the app and various connectors ''' +""" interface between the app and various connectors """ from django.test import TestCase from bookwyrm import models from bookwyrm.connectors import connector_manager -from bookwyrm.connectors.bookwyrm_connector \ - import Connector as BookWyrmConnector -from bookwyrm.connectors.self_connector \ - import Connector as SelfConnector +from bookwyrm.connectors.bookwyrm_connector import Connector as BookWyrmConnector +from bookwyrm.connectors.self_connector import Connector as SelfConnector class ConnectorManager(TestCase): - ''' interface between the app and various connectors ''' + """ interface between the app and various connectors """ + def setUp(self): - ''' we'll need some books and a connector info entry ''' - self.work = models.Work.objects.create( - title='Example Work' - ) + """ we'll need some books and a connector info entry """ + self.work = models.Work.objects.create(title="Example Work") self.edition = models.Edition.objects.create( - title='Example Edition', - parent_work=self.work + title="Example Edition", parent_work=self.work ) self.work.default_edition = self.edition self.work.save() self.connector = models.Connector.objects.create( - identifier='test_connector', + identifier="test_connector", priority=1, local=True, - connector_file='self_connector', - base_url='http://test.com/', - books_url='http://test.com/', - covers_url='http://test.com/', + connector_file="self_connector", + base_url="http://test.com/", + books_url="http://test.com/", + covers_url="http://test.com/", ) - def test_get_or_create_connector(self): - ''' loads a connector if the data source is known or creates one ''' - remote_id = 'https://example.com/object/1' + """ loads a connector if the data source is known or creates one """ + remote_id = "https://example.com/object/1" connector = connector_manager.get_or_create_connector(remote_id) self.assertIsInstance(connector, BookWyrmConnector) - self.assertEqual(connector.identifier, 'example.com') - self.assertEqual(connector.base_url, 'https://example.com') + self.assertEqual(connector.identifier, "example.com") + self.assertEqual(connector.base_url, "https://example.com") same_connector = connector_manager.get_or_create_connector(remote_id) self.assertEqual(connector.identifier, same_connector.identifier) def test_get_connectors(self): - ''' load all connectors ''' - remote_id = 'https://example.com/object/1' + """ load all connectors """ + remote_id = "https://example.com/object/1" connector_manager.get_or_create_connector(remote_id) connectors = list(connector_manager.get_connectors()) self.assertEqual(len(connectors), 2) @@ -56,28 +51,28 @@ class ConnectorManager(TestCase): self.assertIsInstance(connectors[1], BookWyrmConnector) def test_search(self): - ''' search all connectors ''' - results = connector_manager.search('Example') + """ search all connectors """ + results = connector_manager.search("Example") self.assertEqual(len(results), 1) - self.assertIsInstance(results[0]['connector'], SelfConnector) - self.assertEqual(len(results[0]['results']), 1) - self.assertEqual(results[0]['results'][0].title, 'Example Edition') + self.assertIsInstance(results[0]["connector"], SelfConnector) + self.assertEqual(len(results[0]["results"]), 1) + self.assertEqual(results[0]["results"][0].title, "Example Edition") def test_local_search(self): - ''' search only the local database ''' - results = connector_manager.local_search('Example') + """ search only the local database """ + results = connector_manager.local_search("Example") self.assertEqual(len(results), 1) - self.assertEqual(results[0].title, 'Example Edition') + self.assertEqual(results[0].title, "Example Edition") def test_first_search_result(self): - ''' only get one search result ''' - result = connector_manager.first_search_result('Example') - self.assertEqual(result.title, 'Example Edition') - no_result = connector_manager.first_search_result('dkjfhg') + """ only get one search result """ + result = connector_manager.first_search_result("Example") + self.assertEqual(result.title, "Example Edition") + no_result = connector_manager.first_search_result("dkjfhg") self.assertIsNone(no_result) def test_load_connector(self): - ''' load a connector object from the database entry ''' + """ load a connector object from the database entry """ connector = connector_manager.load_connector(self.connector) self.assertIsInstance(connector, SelfConnector) - self.assertEqual(connector.identifier, 'test_connector') + self.assertEqual(connector.identifier, "test_connector") diff --git a/bookwyrm/tests/connectors/test_openlibrary_connector.py b/bookwyrm/tests/connectors/test_openlibrary_connector.py index a174300a9..bb018830e 100644 --- a/bookwyrm/tests/connectors/test_openlibrary_connector.py +++ b/bookwyrm/tests/connectors/test_openlibrary_connector.py @@ -1,4 +1,4 @@ -''' testing book data connectors ''' +""" testing book data connectors """ import json import pathlib from unittest.mock import patch @@ -9,253 +9,231 @@ import responses from bookwyrm import models from bookwyrm.connectors.openlibrary import Connector from bookwyrm.connectors.openlibrary import get_languages, get_description -from bookwyrm.connectors.openlibrary import pick_default_edition, \ - get_openlibrary_key +from bookwyrm.connectors.openlibrary import pick_default_edition, get_openlibrary_key from bookwyrm.connectors.abstract_connector import SearchResult from bookwyrm.connectors.connector_manager import ConnectorException class Openlibrary(TestCase): - ''' test loading data from openlibrary.org ''' - def setUp(self): - ''' creates the connector we'll use ''' - models.Connector.objects.create( - identifier='openlibrary.org', - name='OpenLibrary', - connector_file='openlibrary', - base_url='https://openlibrary.org', - books_url='https://openlibrary.org', - covers_url='https://covers.openlibrary.org', - search_url='https://openlibrary.org/search?q=', - isbn_search_url='https://openlibrary.org/isbn', - ) - self.connector = Connector('openlibrary.org') + """ test loading data from openlibrary.org """ - work_file = pathlib.Path(__file__).parent.joinpath( - '../data/ol_work.json') - edition_file = pathlib.Path(__file__).parent.joinpath( - '../data/ol_edition.json') + def setUp(self): + """ creates the connector we'll use """ + models.Connector.objects.create( + identifier="openlibrary.org", + name="OpenLibrary", + connector_file="openlibrary", + base_url="https://openlibrary.org", + books_url="https://openlibrary.org", + covers_url="https://covers.openlibrary.org", + search_url="https://openlibrary.org/search?q=", + isbn_search_url="https://openlibrary.org/isbn", + ) + self.connector = Connector("openlibrary.org") + + work_file = pathlib.Path(__file__).parent.joinpath("../data/ol_work.json") + edition_file = pathlib.Path(__file__).parent.joinpath("../data/ol_edition.json") edition_list_file = pathlib.Path(__file__).parent.joinpath( - '../data/ol_edition_list.json') + "../data/ol_edition_list.json" + ) self.work_data = json.loads(work_file.read_bytes()) self.edition_data = json.loads(edition_file.read_bytes()) self.edition_list_data = json.loads(edition_list_file.read_bytes()) - def test_get_remote_id_from_data(self): - ''' format the remote id from the data ''' - data = {'key': '/work/OL1234W'} + """ format the remote id from the data """ + data = {"key": "/work/OL1234W"} result = self.connector.get_remote_id_from_data(data) - self.assertEqual(result, 'https://openlibrary.org/work/OL1234W') + self.assertEqual(result, "https://openlibrary.org/work/OL1234W") # error handlding with self.assertRaises(ConnectorException): self.connector.get_remote_id_from_data({}) - def test_is_work_data(self): - ''' detect if the loaded json is a work ''' + """ detect if the loaded json is a work """ self.assertEqual(self.connector.is_work_data(self.work_data), True) self.assertEqual(self.connector.is_work_data(self.edition_data), False) - @responses.activate def test_get_edition_from_work_data(self): - ''' loads a list of editions ''' - data = {'key': '/work/OL1234W'} + """ loads a list of editions """ + data = {"key": "/work/OL1234W"} responses.add( responses.GET, - 'https://openlibrary.org/work/OL1234W/editions', - json={'entries': []}, - status=200) - with patch('bookwyrm.connectors.openlibrary.pick_default_edition') \ - as pick_edition: - pick_edition.return_value = 'hi' + "https://openlibrary.org/work/OL1234W/editions", + json={"entries": []}, + status=200, + ) + with patch( + "bookwyrm.connectors.openlibrary.pick_default_edition" + ) as pick_edition: + pick_edition.return_value = "hi" result = self.connector.get_edition_from_work_data(data) - self.assertEqual(result, 'hi') - + self.assertEqual(result, "hi") @responses.activate def test_get_work_from_edition_data(self): - ''' loads a list of editions ''' - data = {'works': [{'key': '/work/OL1234W'}]} + """ loads a list of editions """ + data = {"works": [{"key": "/work/OL1234W"}]} responses.add( responses.GET, - 'https://openlibrary.org/work/OL1234W', - json={'hi': 'there'}, - status=200) + "https://openlibrary.org/work/OL1234W", + json={"hi": "there"}, + status=200, + ) result = self.connector.get_work_from_edition_data(data) - self.assertEqual(result, {'hi': 'there'}) - + self.assertEqual(result, {"hi": "there"}) @responses.activate def test_get_authors_from_data(self): - ''' find authors in data ''' + """ find authors in data """ responses.add( responses.GET, - 'https://openlibrary.org/authors/OL382982A', + "https://openlibrary.org/authors/OL382982A", json={ "name": "George Elliott", "personal_name": "George Elliott", "last_modified": { "type": "/type/datetime", - "value": "2008-08-31 10:09:33.413686" - }, + "value": "2008-08-31 10:09:33.413686", + }, "key": "/authors/OL453734A", - "type": { - "key": "/type/author" - }, + "type": {"key": "/type/author"}, "id": 1259965, - "revision": 2 + "revision": 2, }, - status=200) + status=200, + ) results = self.connector.get_authors_from_data(self.work_data) result = list(results)[0] self.assertIsInstance(result, models.Author) - self.assertEqual(result.name, 'George Elliott') - self.assertEqual(result.openlibrary_key, 'OL453734A') - + self.assertEqual(result.name, "George Elliott") + self.assertEqual(result.openlibrary_key, "OL453734A") def test_get_cover_url(self): - ''' formats a url that should contain the cover image ''' - blob = ['image'] + """ formats a url that should contain the cover image """ + blob = ["image"] result = self.connector.get_cover_url(blob) - self.assertEqual( - result, 'https://covers.openlibrary.org/b/id/image-L.jpg') + self.assertEqual(result, "https://covers.openlibrary.org/b/id/image-L.jpg") def test_parse_search_result(self): - ''' extract the results from the search json response ''' - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ol_search.json') + """ extract the results from the search json response """ + datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json") search_data = json.loads(datafile.read_bytes()) result = self.connector.parse_search_data(search_data) self.assertIsInstance(result, list) self.assertEqual(len(result), 2) - def test_format_search_result(self): - ''' translate json from openlibrary into SearchResult ''' - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ol_search.json') + """ translate json from openlibrary into SearchResult """ + datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_search_data(search_data) self.assertIsInstance(results, list) result = self.connector.format_search_result(results[0]) self.assertIsInstance(result, SearchResult) - self.assertEqual(result.title, 'This Is How You Lose the Time War') - self.assertEqual( - result.key, 'https://openlibrary.org/works/OL20639540W') - self.assertEqual(result.author, 'Amal El-Mohtar, Max Gladstone') + self.assertEqual(result.title, "This Is How You Lose the Time War") + self.assertEqual(result.key, "https://openlibrary.org/works/OL20639540W") + self.assertEqual(result.author, "Amal El-Mohtar, Max Gladstone") self.assertEqual(result.year, 2019) self.assertEqual(result.connector, self.connector) - def test_parse_isbn_search_result(self): - ''' extract the results from the search json response ''' - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ol_isbn_search.json') + """ extract the results from the search json response """ + datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json") search_data = json.loads(datafile.read_bytes()) result = self.connector.parse_isbn_search_data(search_data) self.assertIsInstance(result, list) self.assertEqual(len(result), 1) - def test_format_isbn_search_result(self): - ''' translate json from openlibrary into SearchResult ''' - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ol_isbn_search.json') + """ translate json from openlibrary into SearchResult """ + datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_isbn_search_data(search_data) self.assertIsInstance(results, list) result = self.connector.format_isbn_search_result(results[0]) self.assertIsInstance(result, SearchResult) - self.assertEqual(result.title, 'Les ombres errantes') - self.assertEqual( - result.key, 'https://openlibrary.org/books/OL16262504M') - self.assertEqual(result.author, 'Pascal Quignard') - self.assertEqual(result.year, '2002') + self.assertEqual(result.title, "Les ombres errantes") + self.assertEqual(result.key, "https://openlibrary.org/books/OL16262504M") + self.assertEqual(result.author, "Pascal Quignard") + self.assertEqual(result.year, "2002") self.assertEqual(result.connector, self.connector) - @responses.activate def test_load_edition_data(self): - ''' format url from key and make request ''' - key = 'OL1234W' + """ format url from key and make request """ + key = "OL1234W" responses.add( responses.GET, - 'https://openlibrary.org/works/OL1234W/editions', - json={'hi': 'there'} + "https://openlibrary.org/works/OL1234W/editions", + json={"hi": "there"}, ) result = self.connector.load_edition_data(key) - self.assertEqual(result, {'hi': 'there'}) - + self.assertEqual(result, {"hi": "there"}) @responses.activate def test_expand_book_data(self): - ''' given a book, get more editions ''' - work = models.Work.objects.create( - title='Test Work', openlibrary_key='OL1234W') - edition = models.Edition.objects.create( - title='Test Edition', parent_work=work) + """ given a book, get more editions """ + work = models.Work.objects.create(title="Test Work", openlibrary_key="OL1234W") + edition = models.Edition.objects.create(title="Test Edition", parent_work=work) responses.add( responses.GET, - 'https://openlibrary.org/works/OL1234W/editions', - json={'entries': []}, + "https://openlibrary.org/works/OL1234W/editions", + json={"entries": []}, ) with patch( - 'bookwyrm.connectors.abstract_connector.AbstractConnector.' \ - 'create_edition_from_data'): + "bookwyrm.connectors.abstract_connector.AbstractConnector." + "create_edition_from_data" + ): self.connector.expand_book_data(edition) self.connector.expand_book_data(work) - def test_get_description(self): - ''' should do some cleanup on the description data ''' - description = get_description(self.work_data['description']) - expected = 'First in the Old Kingdom/Abhorsen series.' + """ should do some cleanup on the description data """ + description = get_description(self.work_data["description"]) + expected = "First in the Old Kingdom/Abhorsen series." self.assertEqual(description, expected) - def test_get_openlibrary_key(self): - ''' extracts the uuid ''' - key = get_openlibrary_key('/books/OL27320736M') - self.assertEqual(key, 'OL27320736M') - + """ extracts the uuid """ + key = get_openlibrary_key("/books/OL27320736M") + self.assertEqual(key, "OL27320736M") def test_get_languages(self): - ''' looks up languages from a list ''' - languages = get_languages(self.edition_data['languages']) - self.assertEqual(languages, ['English']) - + """ looks up languages from a list """ + languages = get_languages(self.edition_data["languages"]) + self.assertEqual(languages, ["English"]) def test_pick_default_edition(self): - ''' detect if the loaded json is an edition ''' - edition = pick_default_edition(self.edition_list_data['entries']) - self.assertEqual(edition['key'], '/books/OL9788823M') - + """ detect if the loaded json is an edition """ + edition = pick_default_edition(self.edition_list_data["entries"]) + self.assertEqual(edition["key"], "/books/OL9788823M") @responses.activate def test_create_edition_from_data(self): - ''' okay but can it actually create an edition with proper metadata ''' - work = models.Work.objects.create(title='Hello') + """ okay but can it actually create an edition with proper metadata """ + work = models.Work.objects.create(title="Hello") responses.add( responses.GET, - 'https://openlibrary.org/authors/OL382982A', - json={'hi': 'there'}, - status=200) - with patch('bookwyrm.connectors.openlibrary.Connector.' \ - 'get_authors_from_data') as mock: + "https://openlibrary.org/authors/OL382982A", + json={"hi": "there"}, + status=200, + ) + with patch( + "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" + ) as mock: mock.return_value = [] - result = self.connector.create_edition_from_data( - work, self.edition_data) + result = self.connector.create_edition_from_data(work, self.edition_data) self.assertEqual(result.parent_work, work) - self.assertEqual(result.title, 'Sabriel') - self.assertEqual(result.isbn_10, '0060273224') + self.assertEqual(result.title, "Sabriel") + self.assertEqual(result.isbn_10, "0060273224") self.assertIsNotNone(result.description) - self.assertEqual(result.languages[0], 'English') - self.assertEqual(result.publishers[0], 'Harper Trophy') + self.assertEqual(result.languages[0], "English") + self.assertEqual(result.publishers[0], "Harper Trophy") self.assertEqual(result.pages, 491) - self.assertEqual(result.subjects[0], 'Fantasy.') - self.assertEqual(result.physical_format, 'Hardcover') + self.assertEqual(result.subjects[0], "Fantasy.") + self.assertEqual(result.physical_format, "Hardcover") diff --git a/bookwyrm/tests/connectors/test_self_connector.py b/bookwyrm/tests/connectors/test_self_connector.py index 0fc789556..9925f5943 100644 --- a/bookwyrm/tests/connectors/test_self_connector.py +++ b/bookwyrm/tests/connectors/test_self_connector.py @@ -1,4 +1,4 @@ -''' testing book data connectors ''' +""" testing book data connectors """ import datetime from django.test import TestCase from django.utils import timezone @@ -9,100 +9,98 @@ from bookwyrm.settings import DOMAIN class SelfConnector(TestCase): - ''' just uses local data ''' + """ just uses local data """ + def setUp(self): - ''' creating the connector ''' + """ creating the connector """ models.Connector.objects.create( identifier=DOMAIN, - name='Local', + name="Local", local=True, - connector_file='self_connector', - base_url='https://%s' % DOMAIN, - books_url='https://%s/book' % DOMAIN, - covers_url='https://%s/images/covers' % DOMAIN, - search_url='https://%s/search?q=' % DOMAIN, + connector_file="self_connector", + base_url="https://%s" % DOMAIN, + books_url="https://%s/book" % DOMAIN, + covers_url="https://%s/images/covers" % DOMAIN, + search_url="https://%s/search?q=" % DOMAIN, priority=1, ) self.connector = Connector(DOMAIN) - def test_format_search_result(self): - ''' create a SearchResult ''' - author = models.Author.objects.create(name='Anonymous') + """ create a SearchResult """ + author = models.Author.objects.create(name="Anonymous") edition = models.Edition.objects.create( - title='Edition of Example Work', + title="Edition of Example Work", published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc), ) edition.authors.add(author) - result = self.connector.search('Edition of Example')[0] - self.assertEqual(result.title, 'Edition of Example Work') + result = self.connector.search("Edition of Example")[0] + self.assertEqual(result.title, "Edition of Example Work") self.assertEqual(result.key, edition.remote_id) - self.assertEqual(result.author, 'Anonymous') + self.assertEqual(result.author, "Anonymous") self.assertEqual(result.year, 1980) self.assertEqual(result.connector, self.connector) - def test_search_rank(self): - ''' prioritize certain results ''' - author = models.Author.objects.create(name='Anonymous') + """ prioritize certain results """ + author = models.Author.objects.create(name="Anonymous") edition = models.Edition.objects.create( - title='Edition of Example Work', + title="Edition of Example Work", published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc), - parent_work=models.Work.objects.create(title='') + parent_work=models.Work.objects.create(title=""), ) # author text is rank C edition.authors.add(author) # series is rank D models.Edition.objects.create( - title='Another Edition', - series='Anonymous', - parent_work=models.Work.objects.create(title='') + title="Another Edition", + series="Anonymous", + parent_work=models.Work.objects.create(title=""), ) # subtitle is rank B models.Edition.objects.create( - title='More Editions', - subtitle='The Anonymous Edition', - parent_work=models.Work.objects.create(title='') + title="More Editions", + subtitle="The Anonymous Edition", + parent_work=models.Work.objects.create(title=""), ) # title is rank A - models.Edition.objects.create(title='Anonymous') + models.Edition.objects.create(title="Anonymous") # doesn't rank in this search edition = models.Edition.objects.create( - title='An Edition', - parent_work=models.Work.objects.create(title='') + title="An Edition", parent_work=models.Work.objects.create(title="") ) - results = self.connector.search('Anonymous') + results = self.connector.search("Anonymous") self.assertEqual(len(results), 3) - self.assertEqual(results[0].title, 'Anonymous') - self.assertEqual(results[1].title, 'More Editions') - self.assertEqual(results[2].title, 'Edition of Example Work') - + self.assertEqual(results[0].title, "Anonymous") + self.assertEqual(results[1].title, "More Editions") + self.assertEqual(results[2].title, "Edition of Example Work") def test_search_multiple_editions(self): - ''' it should get rid of duplicate editions for the same work ''' - work = models.Work.objects.create(title='Work Title') + """ it should get rid of duplicate editions for the same work """ + work = models.Work.objects.create(title="Work Title") edition_1 = models.Edition.objects.create( - title='Edition 1 Title', parent_work=work) + title="Edition 1 Title", parent_work=work + ) edition_2 = models.Edition.objects.create( - title='Edition 2 Title', parent_work=work) - edition_3 = models.Edition.objects.create( - title='Fish', parent_work=work) + title="Edition 2 Title", parent_work=work + ) + edition_3 = models.Edition.objects.create(title="Fish", parent_work=work) work.default_edition = edition_2 work.save() # pick the best edition - results = self.connector.search('Edition 1 Title') + results = self.connector.search("Edition 1 Title") self.assertEqual(len(results), 1) self.assertEqual(results[0].key, edition_1.remote_id) # pick the default edition when no match is best - results = self.connector.search('Edition Title') + results = self.connector.search("Edition Title") self.assertEqual(len(results), 1) self.assertEqual(results[0].key, edition_2.remote_id) # only matches one edition, so no deduplication takes place - results = self.connector.search('Fish') + results = self.connector.search("Fish") self.assertEqual(len(results), 1) self.assertEqual(results[0].key, edition_3.remote_id) diff --git a/bookwyrm/tests/models/test_activitypub_mixin.py b/bookwyrm/tests/models/test_activitypub_mixin.py index 11b944d9a..930f3a533 100644 --- a/bookwyrm/tests/models/test_activitypub_mixin.py +++ b/bookwyrm/tests/models/test_activitypub_mixin.py @@ -1,4 +1,4 @@ -''' testing model activitypub utilities ''' +""" testing model activitypub utilities """ from unittest.mock import patch from collections import namedtuple from dataclasses import dataclass @@ -12,238 +12,249 @@ from bookwyrm.models import base_model from bookwyrm.models.activitypub_mixin import ActivitypubMixin from bookwyrm.models.activitypub_mixin import ActivityMixin, ObjectMixin + class ActivitypubMixins(TestCase): - ''' functionality shared across models ''' + """ functionality shared across models """ + def setUp(self): - ''' shared data ''' + """ shared data """ self.local_user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse') - self.local_user.remote_id = 'http://example.com/a/b' + "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" + ) + self.local_user.remote_id = "http://example.com/a/b" self.local_user.save(broadcast=False) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'ratword', + "rat", + "rat@rat.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) self.object_mock = { - 'to': 'to field', 'cc': 'cc field', - 'content': 'hi', 'id': 'bip', 'type': 'Test', - 'published': '2020-12-04T17:52:22.623807+00:00', + "to": "to field", + "cc": "cc field", + "content": "hi", + "id": "bip", + "type": "Test", + "published": "2020-12-04T17:52:22.623807+00:00", } - # ActivitypubMixin def test_to_activity(self): - ''' model to ActivityPub json ''' + """ model to ActivityPub json """ + @dataclass(init=False) class TestActivity(ActivityObject): - ''' real simple mock ''' - type: str = 'Test' + """ real simple mock """ + + type: str = "Test" class TestModel(ActivitypubMixin, base_model.BookWyrmModel): - ''' real simple mock model because BookWyrmModel is abstract ''' + """ real simple mock model because BookWyrmModel is abstract """ instance = TestModel() - instance.remote_id = 'https://www.example.com/test' + instance.remote_id = "https://www.example.com/test" instance.activity_serializer = TestActivity activity = instance.to_activity() self.assertIsInstance(activity, dict) - self.assertEqual(activity['id'], 'https://www.example.com/test') - self.assertEqual(activity['type'], 'Test') - + self.assertEqual(activity["id"], "https://www.example.com/test") + self.assertEqual(activity["type"], "Test") def test_find_existing_by_remote_id(self): - ''' attempt to match a remote id to an object in the db ''' + """ attempt to match a remote id to an object in the db """ # uses a different remote id scheme # this isn't really part of this test directly but it's helpful to state book = models.Edition.objects.create( - title='Test Edition', remote_id='http://book.com/book') + title="Test Edition", remote_id="http://book.com/book" + ) - self.assertEqual(book.origin_id, 'http://book.com/book') - self.assertNotEqual(book.remote_id, 'http://book.com/book') + self.assertEqual(book.origin_id, "http://book.com/book") + self.assertNotEqual(book.remote_id, "http://book.com/book") # uses subclasses - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.Comment.objects.create( - user=self.local_user, content='test status', book=book, \ - remote_id='https://comment.net') + user=self.local_user, + content="test status", + book=book, + remote_id="https://comment.net", + ) - result = models.User.find_existing_by_remote_id('hi') + result = models.User.find_existing_by_remote_id("hi") self.assertIsNone(result) - result = models.User.find_existing_by_remote_id( - 'http://example.com/a/b') + result = models.User.find_existing_by_remote_id("http://example.com/a/b") self.assertEqual(result, self.local_user) # test using origin id - result = models.Edition.find_existing_by_remote_id( - 'http://book.com/book') + result = models.Edition.find_existing_by_remote_id("http://book.com/book") self.assertEqual(result, book) # test subclass match - result = models.Status.find_existing_by_remote_id( - 'https://comment.net') - + result = models.Status.find_existing_by_remote_id("https://comment.net") def test_find_existing(self): - ''' match a blob of data to a model ''' + """ match a blob of data to a model """ book = models.Edition.objects.create( - title='Test edition', - openlibrary_key='OL1234', + title="Test edition", + openlibrary_key="OL1234", ) - result = models.Edition.find_existing( - {'openlibraryKey': 'OL1234'}) + result = models.Edition.find_existing({"openlibraryKey": "OL1234"}) self.assertEqual(result, book) - def test_get_recipients_public_object(self): - ''' determines the recipients for an object's broadcast ''' - MockSelf = namedtuple('Self', ('privacy')) - mock_self = MockSelf('public') + """ determines the recipients for an object's broadcast """ + MockSelf = namedtuple("Self", ("privacy")) + mock_self = MockSelf("public") recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 1) self.assertEqual(recipients[0], self.remote_user.inbox) - def test_get_recipients_public_user_object_no_followers(self): - ''' determines the recipients for a user's object broadcast ''' - MockSelf = namedtuple('Self', ('privacy', 'user')) - mock_self = MockSelf('public', self.local_user) + """ determines the recipients for a user's object broadcast """ + MockSelf = namedtuple("Self", ("privacy", "user")) + mock_self = MockSelf("public", self.local_user) recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 0) - def test_get_recipients_public_user_object(self): - ''' determines the recipients for a user's object broadcast ''' - MockSelf = namedtuple('Self', ('privacy', 'user')) - mock_self = MockSelf('public', self.local_user) + """ determines the recipients for a user's object broadcast """ + MockSelf = namedtuple("Self", ("privacy", "user")) + mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 1) self.assertEqual(recipients[0], self.remote_user.inbox) - def test_get_recipients_public_user_object_with_mention(self): - ''' determines the recipients for a user's object broadcast ''' - MockSelf = namedtuple('Self', ('privacy', 'user')) - mock_self = MockSelf('public', self.local_user) + """ determines the recipients for a user's object broadcast """ + MockSelf = namedtuple("Self", ("privacy", "user")) + mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user( - 'nutria', 'nutria@nutria.com', 'nutriaword', + "nutria", + "nutria@nutria.com", + "nutriaword", local=False, - remote_id='https://example.com/users/nutria', - inbox='https://example.com/users/nutria/inbox', - outbox='https://example.com/users/nutria/outbox', + remote_id="https://example.com/users/nutria", + inbox="https://example.com/users/nutria/inbox", + outbox="https://example.com/users/nutria/outbox", ) - MockSelf = namedtuple('Self', ('privacy', 'user', 'recipients')) - mock_self = MockSelf('public', self.local_user, [another_remote_user]) + MockSelf = namedtuple("Self", ("privacy", "user", "recipients")) + mock_self = MockSelf("public", self.local_user, [another_remote_user]) recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 2) self.assertEqual(recipients[0], another_remote_user.inbox) self.assertEqual(recipients[1], self.remote_user.inbox) - def test_get_recipients_direct(self): - ''' determines the recipients for a user's object broadcast ''' - MockSelf = namedtuple('Self', ('privacy', 'user')) - mock_self = MockSelf('public', self.local_user) + """ determines the recipients for a user's object broadcast """ + MockSelf = namedtuple("Self", ("privacy", "user")) + mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user( - 'nutria', 'nutria@nutria.com', 'nutriaword', + "nutria", + "nutria@nutria.com", + "nutriaword", local=False, - remote_id='https://example.com/users/nutria', - inbox='https://example.com/users/nutria/inbox', - outbox='https://example.com/users/nutria/outbox', + remote_id="https://example.com/users/nutria", + inbox="https://example.com/users/nutria/inbox", + outbox="https://example.com/users/nutria/outbox", ) - MockSelf = namedtuple('Self', ('privacy', 'user', 'recipients')) - mock_self = MockSelf('direct', self.local_user, [another_remote_user]) + MockSelf = namedtuple("Self", ("privacy", "user", "recipients")) + mock_self = MockSelf("direct", self.local_user, [another_remote_user]) recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 1) self.assertEqual(recipients[0], another_remote_user.inbox) - def test_get_recipients_combine_inboxes(self): - ''' should combine users with the same shared_inbox ''' - self.remote_user.shared_inbox = 'http://example.com/inbox' + """ should combine users with the same shared_inbox """ + self.remote_user.shared_inbox = "http://example.com/inbox" self.remote_user.save(broadcast=False) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user( - 'nutria', 'nutria@nutria.com', 'nutriaword', + "nutria", + "nutria@nutria.com", + "nutriaword", local=False, - remote_id='https://example.com/users/nutria', - inbox='https://example.com/users/nutria/inbox', - shared_inbox='http://example.com/inbox', - outbox='https://example.com/users/nutria/outbox', + remote_id="https://example.com/users/nutria", + inbox="https://example.com/users/nutria/inbox", + shared_inbox="http://example.com/inbox", + outbox="https://example.com/users/nutria/outbox", ) - MockSelf = namedtuple('Self', ('privacy', 'user')) - mock_self = MockSelf('public', self.local_user) + MockSelf = namedtuple("Self", ("privacy", "user")) + mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) self.local_user.followers.add(another_remote_user) recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 1) - self.assertEqual(recipients[0], 'http://example.com/inbox') - + self.assertEqual(recipients[0], "http://example.com/inbox") def test_get_recipients_software(self): - ''' should differentiate between bookwyrm and other remote users ''' - with patch('bookwyrm.models.user.set_remote_server.delay'): + """ should differentiate between bookwyrm and other remote users """ + with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user( - 'nutria', 'nutria@nutria.com', 'nutriaword', + "nutria", + "nutria@nutria.com", + "nutriaword", local=False, - remote_id='https://example.com/users/nutria', - inbox='https://example.com/users/nutria/inbox', - outbox='https://example.com/users/nutria/outbox', + remote_id="https://example.com/users/nutria", + inbox="https://example.com/users/nutria/inbox", + outbox="https://example.com/users/nutria/outbox", bookwyrm_user=False, ) - MockSelf = namedtuple('Self', ('privacy', 'user')) - mock_self = MockSelf('public', self.local_user) + MockSelf = namedtuple("Self", ("privacy", "user")) + mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) self.local_user.followers.add(another_remote_user) recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 2) - recipients = ActivitypubMixin.get_recipients( - mock_self, software='bookwyrm') + recipients = ActivitypubMixin.get_recipients(mock_self, software="bookwyrm") self.assertEqual(len(recipients), 1) self.assertEqual(recipients[0], self.remote_user.inbox) - recipients = ActivitypubMixin.get_recipients( - mock_self, software='other') + recipients = ActivitypubMixin.get_recipients(mock_self, software="other") self.assertEqual(len(recipients), 1) self.assertEqual(recipients[0], another_remote_user.inbox) - # ObjectMixin def test_object_save_create(self): - ''' should save uneventufully when broadcast is disabled ''' + """ should save uneventufully when broadcast is disabled """ + class Success(Exception): - ''' this means we got to the right method ''' + """ this means we got to the right method """ class ObjectModel(ObjectMixin, base_model.BookWyrmModel): - ''' real simple mock model because BookWyrmModel is abstract ''' - user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE) + """ real simple mock model because BookWyrmModel is abstract """ + + user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) + def save(self, *args, **kwargs): - with patch('django.db.models.Model.save'): + with patch("django.db.models.Model.save"): super().save(*args, **kwargs) - def broadcast(self, activity, sender, **kwargs):#pylint: disable=arguments-differ - ''' do something ''' + + def broadcast( + self, activity, sender, **kwargs + ): # pylint: disable=arguments-differ + """ do something """ raise Success() - def to_create_activity(self, user):#pylint: disable=arguments-differ + + def to_create_activity(self, user): # pylint: disable=arguments-differ return {} with self.assertRaises(Success): @@ -253,20 +264,24 @@ class ActivitypubMixins(TestCase): ObjectModel(user=self.local_user).save(broadcast=False) ObjectModel(user=None).save() - def test_object_save_update(self): - ''' should save uneventufully when broadcast is disabled ''' + """ should save uneventufully when broadcast is disabled """ + class Success(Exception): - ''' this means we got to the right method ''' + """ this means we got to the right method """ class UpdateObjectModel(ObjectMixin, base_model.BookWyrmModel): - ''' real simple mock model because BookWyrmModel is abstract ''' - user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE) + """ real simple mock model because BookWyrmModel is abstract """ + + user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) last_edited_by = models.fields.ForeignKey( - 'User', on_delete=db.models.CASCADE) + "User", on_delete=db.models.CASCADE + ) + def save(self, *args, **kwargs): - with patch('django.db.models.Model.save'): + with patch("django.db.models.Model.save"): super().save(*args, **kwargs) + def to_update_activity(self, user): raise Success() @@ -275,87 +290,71 @@ class ActivitypubMixins(TestCase): with self.assertRaises(Success): UpdateObjectModel(id=1, last_edited_by=self.local_user).save() - def test_object_save_delete(self): - ''' should create delete activities when objects are deleted by flag ''' + """ should create delete activities when objects are deleted by flag """ + class ActivitySuccess(Exception): - ''' this means we got to the right method ''' + """ this means we got to the right method """ class DeletableObjectModel(ObjectMixin, base_model.BookWyrmModel): - ''' real simple mock model because BookWyrmModel is abstract ''' - user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE) + """ real simple mock model because BookWyrmModel is abstract """ + + user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) deleted = models.fields.BooleanField() + def save(self, *args, **kwargs): - with patch('django.db.models.Model.save'): + with patch("django.db.models.Model.save"): super().save(*args, **kwargs) + def to_delete_activity(self, user): raise ActivitySuccess() with self.assertRaises(ActivitySuccess): - DeletableObjectModel( - id=1, user=self.local_user, deleted=True).save() - + DeletableObjectModel(id=1, user=self.local_user, deleted=True).save() def test_to_delete_activity(self): - ''' wrapper for Delete activity ''' - MockSelf = namedtuple('Self', ('remote_id', 'to_activity')) + """ wrapper for Delete activity """ + MockSelf = namedtuple("Self", ("remote_id", "to_activity")) mock_self = MockSelf( - 'https://example.com/status/1', - lambda *args: self.object_mock + "https://example.com/status/1", lambda *args: self.object_mock ) - activity = ObjectMixin.to_delete_activity( - mock_self, self.local_user) + activity = ObjectMixin.to_delete_activity(mock_self, self.local_user) + self.assertEqual(activity["id"], "https://example.com/status/1/activity") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["type"], "Delete") + self.assertEqual(activity["to"], ["%s/followers" % self.local_user.remote_id]) self.assertEqual( - activity['id'], - 'https://example.com/status/1/activity' + activity["cc"], ["https://www.w3.org/ns/activitystreams#Public"] ) - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['type'], 'Delete') - self.assertEqual( - activity['to'], - ['%s/followers' % self.local_user.remote_id]) - self.assertEqual( - activity['cc'], - ['https://www.w3.org/ns/activitystreams#Public']) - def test_to_update_activity(self): - ''' ditto above but for Update ''' - MockSelf = namedtuple('Self', ('remote_id', 'to_activity')) + """ ditto above but for Update """ + MockSelf = namedtuple("Self", ("remote_id", "to_activity")) mock_self = MockSelf( - 'https://example.com/status/1', - lambda *args: self.object_mock + "https://example.com/status/1", lambda *args: self.object_mock ) - activity = ObjectMixin.to_update_activity( - mock_self, self.local_user) + activity = ObjectMixin.to_update_activity(mock_self, self.local_user) self.assertIsNotNone( - re.match( - r'^https:\/\/example\.com\/status\/1#update\/.*', - activity['id'] - ) + re.match(r"^https:\/\/example\.com\/status\/1#update\/.*", activity["id"]) ) - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['type'], 'Update') + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["type"], "Update") self.assertEqual( - activity['to'], - ['https://www.w3.org/ns/activitystreams#Public']) - self.assertIsInstance(activity['object'], dict) - + activity["to"], ["https://www.w3.org/ns/activitystreams#Public"] + ) + self.assertIsInstance(activity["object"], dict) # Activity mixin def test_to_undo_activity(self): - ''' and again, for Undo ''' - MockSelf = namedtuple('Self', ('remote_id', 'to_activity', 'user')) + """ and again, for Undo """ + MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user")) mock_self = MockSelf( - 'https://example.com/status/1', + "https://example.com/status/1", lambda *args: self.object_mock, self.local_user, ) activity = ActivityMixin.to_undo_activity(mock_self) - self.assertEqual( - activity['id'], - 'https://example.com/status/1#undo' - ) - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['type'], 'Undo') - self.assertIsInstance(activity['object'], dict) + self.assertEqual(activity["id"], "https://example.com/status/1#undo") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["type"], "Undo") + self.assertIsInstance(activity["object"], dict) diff --git a/bookwyrm/tests/models/test_base_model.py b/bookwyrm/tests/models/test_base_model.py index ab388efe0..4479d1560 100644 --- a/bookwyrm/tests/models/test_base_model.py +++ b/bookwyrm/tests/models/test_base_model.py @@ -1,42 +1,41 @@ -''' testing models ''' +""" testing models """ from django.test import TestCase from bookwyrm import models from bookwyrm.models import base_model from bookwyrm.settings import DOMAIN + class BaseModel(TestCase): - ''' functionality shared across models ''' + """ functionality shared across models """ + def test_remote_id(self): - ''' these should be generated ''' + """ these should be generated """ instance = base_model.BookWyrmModel() instance.id = 1 expected = instance.get_remote_id() - self.assertEqual(expected, 'https://%s/bookwyrmmodel/1' % DOMAIN) + self.assertEqual(expected, "https://%s/bookwyrmmodel/1" % DOMAIN) def test_remote_id_with_user(self): - ''' format of remote id when there's a user object ''' + """ format of remote id when there's a user object """ user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse') + "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" + ) instance = base_model.BookWyrmModel() instance.user = user instance.id = 1 expected = instance.get_remote_id() - self.assertEqual( - expected, - 'https://%s/user/mouse/bookwyrmmodel/1' % DOMAIN) + self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN) def test_execute_after_save(self): - ''' this function sets remote ids after creation ''' + """ this function sets remote ids after creation """ # using Work because it BookWrymModel is abstract and this requires save # Work is a relatively not-fancy model. - instance = models.Work.objects.create(title='work title') + instance = models.Work.objects.create(title="work title") instance.remote_id = None base_model.execute_after_save(None, instance, True) self.assertEqual( - instance.remote_id, - 'https://%s/book/%d' % (DOMAIN, instance.id) + instance.remote_id, "https://%s/book/%d" % (DOMAIN, instance.id) ) # shouldn't set remote_id if it's not created diff --git a/bookwyrm/tests/models/test_book_model.py b/bookwyrm/tests/models/test_book_model.py index b4a099d05..14ab0c572 100644 --- a/bookwyrm/tests/models/test_book_model.py +++ b/bookwyrm/tests/models/test_book_model.py @@ -1,4 +1,4 @@ -''' testing models ''' +""" testing models """ from dateutil.parser import parse from django.test import TestCase from django.utils import timezone @@ -8,88 +8,80 @@ from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10 class Book(TestCase): - ''' not too much going on in the books model but here we are ''' + """ not too much going on in the books model but here we are """ + def setUp(self): - ''' we'll need some books ''' + """ we'll need some books """ self.work = models.Work.objects.create( - title='Example Work', - remote_id='https://example.com/book/1' + title="Example Work", remote_id="https://example.com/book/1" ) self.first_edition = models.Edition.objects.create( - title='Example Edition', + title="Example Edition", parent_work=self.work, ) self.second_edition = models.Edition.objects.create( - title='Another Example Edition', + title="Another Example Edition", parent_work=self.work, ) def test_remote_id(self): - ''' fanciness with remote/origin ids ''' - remote_id = 'https://%s/book/%d' % (settings.DOMAIN, self.work.id) + """ fanciness with remote/origin ids """ + remote_id = "https://%s/book/%d" % (settings.DOMAIN, self.work.id) self.assertEqual(self.work.get_remote_id(), remote_id) self.assertEqual(self.work.remote_id, remote_id) def test_create_book(self): - ''' you shouldn't be able to create Books (only editions and works) ''' - self.assertRaises( - ValueError, - models.Book.objects.create, - title='Invalid Book' - ) + """ you shouldn't be able to create Books (only editions and works) """ + self.assertRaises(ValueError, models.Book.objects.create, title="Invalid Book") def test_isbn_10_to_13(self): - ''' checksums and so on ''' - isbn_10 = '178816167X' + """ checksums and so on """ + isbn_10 = "178816167X" isbn_13 = isbn_10_to_13(isbn_10) - self.assertEqual(isbn_13, '9781788161671') + self.assertEqual(isbn_13, "9781788161671") - isbn_10 = '1-788-16167-X' + isbn_10 = "1-788-16167-X" isbn_13 = isbn_10_to_13(isbn_10) - self.assertEqual(isbn_13, '9781788161671') - + self.assertEqual(isbn_13, "9781788161671") def test_isbn_13_to_10(self): - ''' checksums and so on ''' - isbn_13 = '9781788161671' + """ checksums and so on """ + isbn_13 = "9781788161671" isbn_10 = isbn_13_to_10(isbn_13) - self.assertEqual(isbn_10, '178816167X') + self.assertEqual(isbn_10, "178816167X") - isbn_13 = '978-1788-16167-1' + isbn_13 = "978-1788-16167-1" isbn_10 = isbn_13_to_10(isbn_13) - self.assertEqual(isbn_10, '178816167X') - + self.assertEqual(isbn_10, "178816167X") def test_get_edition_info(self): - ''' text slug about an edition ''' - book = models.Edition.objects.create(title='Test Edition') - self.assertEqual(book.edition_info, '') + """ text slug about an edition """ + book = models.Edition.objects.create(title="Test Edition") + self.assertEqual(book.edition_info, "") - book.physical_format = 'worm' + book.physical_format = "worm" book.save() - self.assertEqual(book.edition_info, 'worm') + self.assertEqual(book.edition_info, "worm") - book.languages = ['English'] + book.languages = ["English"] book.save() - self.assertEqual(book.edition_info, 'worm') + self.assertEqual(book.edition_info, "worm") - book.languages = ['Glorbish', 'English'] + book.languages = ["Glorbish", "English"] book.save() - self.assertEqual(book.edition_info, 'worm, Glorbish language') + self.assertEqual(book.edition_info, "worm, Glorbish language") - book.published_date = timezone.make_aware(parse('2020')) + book.published_date = timezone.make_aware(parse("2020")) book.save() - self.assertEqual(book.edition_info, 'worm, Glorbish language, 2020') - self.assertEqual( - book.alt_text, 'Test Edition (worm, Glorbish language, 2020)') - + self.assertEqual(book.edition_info, "worm, Glorbish language, 2020") + self.assertEqual(book.alt_text, "Test Edition (worm, Glorbish language, 2020)") def test_get_rank(self): - ''' sets the data quality index for the book ''' + """ sets the data quality index for the book """ # basic rank self.assertEqual(self.first_edition.edition_rank, 0) - self.first_edition.description = 'hi' + self.first_edition.description = "hi" self.first_edition.save() self.assertEqual(self.first_edition.edition_rank, 1) diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py index 24c0fb021..522d16f94 100644 --- a/bookwyrm/tests/models/test_fields.py +++ b/bookwyrm/tests/models/test_fields.py @@ -1,4 +1,4 @@ -''' testing models ''' +""" testing models """ from io import BytesIO from collections import namedtuple from dataclasses import dataclass @@ -23,408 +23,388 @@ from bookwyrm.models import fields, User, Status from bookwyrm.models.base_model import BookWyrmModel from bookwyrm.models.activitypub_mixin import ActivitypubMixin -#pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods class ActivitypubFields(TestCase): - ''' overwrites standard model feilds to work with activitypub ''' + """ overwrites standard model feilds to work with activitypub """ + def test_validate_remote_id(self): - ''' should look like a url ''' - self.assertIsNone(fields.validate_remote_id('http://www.example.com')) - self.assertIsNone(fields.validate_remote_id('https://www.example.com')) - self.assertIsNone(fields.validate_remote_id('http://exle.com/dlg-23/x')) + """ should look like a url """ + self.assertIsNone(fields.validate_remote_id("http://www.example.com")) + self.assertIsNone(fields.validate_remote_id("https://www.example.com")) + self.assertIsNone(fields.validate_remote_id("http://exle.com/dlg-23/x")) self.assertRaises( - ValidationError, fields.validate_remote_id, - 'http:/example.com/dlfjg-23/x') + ValidationError, fields.validate_remote_id, "http:/example.com/dlfjg-23/x" + ) self.assertRaises( - ValidationError, fields.validate_remote_id, - 'www.example.com/dlfjg-23/x') + ValidationError, fields.validate_remote_id, "www.example.com/dlfjg-23/x" + ) self.assertRaises( - ValidationError, fields.validate_remote_id, - 'http://www.example.com/dlfjg 23/x') + ValidationError, + fields.validate_remote_id, + "http://www.example.com/dlfjg 23/x", + ) def test_activitypub_field_mixin(self): - ''' generic mixin with super basic to and from functionality ''' + """ generic mixin with super basic to and from functionality """ instance = fields.ActivitypubFieldMixin() - self.assertEqual(instance.field_to_activity('fish'), 'fish') - self.assertEqual(instance.field_from_activity('fish'), 'fish') + self.assertEqual(instance.field_to_activity("fish"), "fish") + self.assertEqual(instance.field_from_activity("fish"), "fish") self.assertFalse(instance.deduplication_field) instance = fields.ActivitypubFieldMixin( - activitypub_wrapper='endpoints', activitypub_field='outbox' + activitypub_wrapper="endpoints", activitypub_field="outbox" ) - self.assertEqual( - instance.field_to_activity('fish'), - {'outbox': 'fish'} - ) - self.assertEqual( - instance.field_from_activity({'outbox': 'fish'}), - 'fish' - ) - self.assertEqual(instance.get_activitypub_field(), 'endpoints') + self.assertEqual(instance.field_to_activity("fish"), {"outbox": "fish"}) + self.assertEqual(instance.field_from_activity({"outbox": "fish"}), "fish") + self.assertEqual(instance.get_activitypub_field(), "endpoints") instance = fields.ActivitypubFieldMixin() - instance.name = 'snake_case_name' - self.assertEqual(instance.get_activitypub_field(), 'snakeCaseName') + instance.name = "snake_case_name" + self.assertEqual(instance.get_activitypub_field(), "snakeCaseName") def test_set_field_from_activity(self): - ''' setter from entire json blob ''' + """ setter from entire json blob """ + @dataclass class TestModel: - ''' real simple mock ''' + """ real simple mock """ + field_name: str - mock_model = TestModel(field_name='bip') - TestActivity = namedtuple('test', ('fieldName', 'unrelated')) - data = TestActivity(fieldName='hi', unrelated='bfkjh') + mock_model = TestModel(field_name="bip") + TestActivity = namedtuple("test", ("fieldName", "unrelated")) + data = TestActivity(fieldName="hi", unrelated="bfkjh") instance = fields.ActivitypubFieldMixin() - instance.name = 'field_name' + instance.name = "field_name" instance.set_field_from_activity(mock_model, data) - self.assertEqual(mock_model.field_name, 'hi') + self.assertEqual(mock_model.field_name, "hi") def test_set_activity_from_field(self): - ''' set json field given entire model ''' + """ set json field given entire model """ + @dataclass class TestModel: - ''' real simple mock ''' + """ real simple mock """ + field_name: str unrelated: str - mock_model = TestModel(field_name='bip', unrelated='field') + + mock_model = TestModel(field_name="bip", unrelated="field") instance = fields.ActivitypubFieldMixin() - instance.name = 'field_name' + instance.name = "field_name" data = {} instance.set_activity_from_field(data, mock_model) - self.assertEqual(data['fieldName'], 'bip') + self.assertEqual(data["fieldName"], "bip") def test_remote_id_field(self): - ''' just sets some defaults on charfield ''' + """ just sets some defaults on charfield """ instance = fields.RemoteIdField() self.assertEqual(instance.max_length, 255) self.assertTrue(instance.deduplication_field) with self.assertRaises(ValidationError): - instance.run_validators('http://www.example.com/dlfjg 23/x') + instance.run_validators("http://www.example.com/dlfjg 23/x") def test_username_field(self): - ''' again, just setting defaults on username field ''' + """ again, just setting defaults on username field """ instance = fields.UsernameField() - self.assertEqual(instance.activitypub_field, 'preferredUsername') + self.assertEqual(instance.activitypub_field, "preferredUsername") self.assertEqual(instance.max_length, 150) self.assertEqual(instance.unique, True) with self.assertRaises(ValidationError): - instance.run_validators('mouse') - instance.run_validators('mouseexample.com') - instance.run_validators('mouse@example.c') - instance.run_validators('@example.com') - instance.run_validators('mouse@examplecom') - instance.run_validators('one two@fish.aaaa') - instance.run_validators('a*&@exampke.com') - instance.run_validators('trailingwhite@example.com ') - self.assertIsNone(instance.run_validators('mouse@example.com')) - self.assertIsNone(instance.run_validators('mo-2use@ex3ample.com')) - self.assertIsNone(instance.run_validators('aksdhf@sdkjf-df.cm')) - - self.assertEqual(instance.field_to_activity('test@example.com'), 'test') + instance.run_validators("mouse") + instance.run_validators("mouseexample.com") + instance.run_validators("mouse@example.c") + instance.run_validators("@example.com") + instance.run_validators("mouse@examplecom") + instance.run_validators("one two@fish.aaaa") + instance.run_validators("a*&@exampke.com") + instance.run_validators("trailingwhite@example.com ") + self.assertIsNone(instance.run_validators("mouse@example.com")) + self.assertIsNone(instance.run_validators("mo-2use@ex3ample.com")) + self.assertIsNone(instance.run_validators("aksdhf@sdkjf-df.cm")) + self.assertEqual(instance.field_to_activity("test@example.com"), "test") def test_privacy_field_defaults(self): - ''' post privacy field's many default values ''' + """ post privacy field's many default values """ instance = fields.PrivacyField() self.assertEqual(instance.max_length, 255) self.assertEqual( [c[0] for c in instance.choices], - ['public', 'unlisted', 'followers', 'direct']) - self.assertEqual(instance.default, 'public') + ["public", "unlisted", "followers", "direct"], + ) + self.assertEqual(instance.default, "public") self.assertEqual( - instance.public, 'https://www.w3.org/ns/activitystreams#Public') + instance.public, "https://www.w3.org/ns/activitystreams#Public" + ) def test_privacy_field_set_field_from_activity(self): - ''' translate between to/cc fields and privacy ''' + """ translate between to/cc fields and privacy """ + @dataclass(init=False) class TestActivity(ActivityObject): - ''' real simple mock ''' + """ real simple mock """ + to: List[str] cc: List[str] - id: str = 'http://hi.com' - type: str = 'Test' + id: str = "http://hi.com" + type: str = "Test" class TestPrivacyModel(ActivitypubMixin, BookWyrmModel): - ''' real simple mock model because BookWyrmModel is abstract ''' + """ real simple mock model because BookWyrmModel is abstract """ + privacy_field = fields.PrivacyField() mention_users = fields.TagField(User) user = fields.ForeignKey(User, on_delete=models.CASCADE) - public = 'https://www.w3.org/ns/activitystreams#Public' + public = "https://www.w3.org/ns/activitystreams#Public" data = TestActivity( to=[public], - cc=['bleh'], + cc=["bleh"], ) - model_instance = TestPrivacyModel(privacy_field='direct') - self.assertEqual(model_instance.privacy_field, 'direct') + model_instance = TestPrivacyModel(privacy_field="direct") + self.assertEqual(model_instance.privacy_field, "direct") instance = fields.PrivacyField() - instance.name = 'privacy_field' + instance.name = "privacy_field" instance.set_field_from_activity(model_instance, data) - self.assertEqual(model_instance.privacy_field, 'public') + self.assertEqual(model_instance.privacy_field, "public") - data.to = ['bleh'] + data.to = ["bleh"] data.cc = [] instance.set_field_from_activity(model_instance, data) - self.assertEqual(model_instance.privacy_field, 'direct') + self.assertEqual(model_instance.privacy_field, "direct") - data.to = ['bleh'] - data.cc = [public, 'waah'] + data.to = ["bleh"] + data.cc = [public, "waah"] instance.set_field_from_activity(model_instance, data) - self.assertEqual(model_instance.privacy_field, 'unlisted') + self.assertEqual(model_instance.privacy_field, "unlisted") - - @patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast') + @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") def test_privacy_field_set_activity_from_field(self, _): - ''' translate between to/cc fields and privacy ''' + """ translate between to/cc fields and privacy """ user = User.objects.create_user( - 'rat', 'rat@rat.rat', 'ratword', - local=True, localname='rat') - public = 'https://www.w3.org/ns/activitystreams#Public' - followers = '%s/followers' % user.remote_id + "rat", "rat@rat.rat", "ratword", local=True, localname="rat" + ) + public = "https://www.w3.org/ns/activitystreams#Public" + followers = "%s/followers" % user.remote_id instance = fields.PrivacyField() - instance.name = 'privacy_field' + instance.name = "privacy_field" - model_instance = Status.objects.create(user=user, content='hi') + model_instance = Status.objects.create(user=user, content="hi") activity = {} instance.set_activity_from_field(activity, model_instance) - self.assertEqual(activity['to'], [public]) - self.assertEqual(activity['cc'], [followers]) + self.assertEqual(activity["to"], [public]) + self.assertEqual(activity["cc"], [followers]) model_instance = Status.objects.create( - user=user, content='hi', privacy='unlisted') + user=user, content="hi", privacy="unlisted" + ) activity = {} instance.set_activity_from_field(activity, model_instance) - self.assertEqual(activity['to'], [followers]) - self.assertEqual(activity['cc'], [public]) + self.assertEqual(activity["to"], [followers]) + self.assertEqual(activity["cc"], [public]) model_instance = Status.objects.create( - user=user, content='hi', privacy='followers') + user=user, content="hi", privacy="followers" + ) activity = {} instance.set_activity_from_field(activity, model_instance) - self.assertEqual(activity['to'], [followers]) - self.assertEqual(activity['cc'], []) + self.assertEqual(activity["to"], [followers]) + self.assertEqual(activity["cc"], []) model_instance = Status.objects.create( user=user, - content='hi', - privacy='direct', + content="hi", + privacy="direct", ) model_instance.mention_users.set([user]) activity = {} instance.set_activity_from_field(activity, model_instance) - self.assertEqual(activity['to'], [user.remote_id]) - self.assertEqual(activity['cc'], []) - + self.assertEqual(activity["to"], [user.remote_id]) + self.assertEqual(activity["cc"], []) def test_foreign_key(self): - ''' should be able to format a related model ''' - instance = fields.ForeignKey('User', on_delete=models.CASCADE) - Serializable = namedtuple('Serializable', ('to_activity', 'remote_id')) - item = Serializable(lambda: {'a': 'b'}, 'https://e.b/c') + """ should be able to format a related model """ + instance = fields.ForeignKey("User", on_delete=models.CASCADE) + Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) + item = Serializable(lambda: {"a": "b"}, "https://e.b/c") # returns the remote_id field of the related object - self.assertEqual(instance.field_to_activity(item), 'https://e.b/c') - + self.assertEqual(instance.field_to_activity(item), "https://e.b/c") @responses.activate def test_foreign_key_from_activity_str(self): - ''' create a new object from a foreign key ''' + """ create a new object from a foreign key """ instance = fields.ForeignKey(User, on_delete=models.CASCADE) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_user.json') + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) # don't try to load the user icon - del userdata['icon'] + del userdata["icon"] # it shouldn't match with this unrelated user: unrelated_user = User.objects.create_user( - 'rat', 'rat@rat.rat', 'ratword', - local=True, localname='rat') + "rat", "rat@rat.rat", "ratword", local=True, localname="rat" + ) # test receiving an unknown remote id and loading data responses.add( - responses.GET, - 'https://example.com/user/mouse', - json=userdata, - status=200) - with patch('bookwyrm.models.user.set_remote_server.delay'): - value = instance.field_from_activity( - 'https://example.com/user/mouse') + responses.GET, "https://example.com/user/mouse", json=userdata, status=200 + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + value = instance.field_from_activity("https://example.com/user/mouse") self.assertIsInstance(value, User) self.assertNotEqual(value, unrelated_user) - self.assertEqual(value.remote_id, 'https://example.com/user/mouse') - self.assertEqual(value.name, 'MOUSE?? MOUSE!!') - + self.assertEqual(value.remote_id, "https://example.com/user/mouse") + self.assertEqual(value.name, "MOUSE?? MOUSE!!") def test_foreign_key_from_activity_dict(self): - ''' test recieving activity json ''' + """ test recieving activity json """ instance = fields.ForeignKey(User, on_delete=models.CASCADE) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_user.json') + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) # don't try to load the user icon - del userdata['icon'] + del userdata["icon"] # it shouldn't match with this unrelated user: unrelated_user = User.objects.create_user( - 'rat', 'rat@rat.rat', 'ratword', - local=True, localname='rat') - with patch('bookwyrm.models.user.set_remote_server.delay'): + "rat", "rat@rat.rat", "ratword", local=True, localname="rat" + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): value = instance.field_from_activity(activitypub.Person(**userdata)) self.assertIsInstance(value, User) self.assertNotEqual(value, unrelated_user) - self.assertEqual(value.remote_id, 'https://example.com/user/mouse') - self.assertEqual(value.name, 'MOUSE?? MOUSE!!') + self.assertEqual(value.remote_id, "https://example.com/user/mouse") + self.assertEqual(value.name, "MOUSE?? MOUSE!!") # et cetera but we're not testing serializing user json - def test_foreign_key_from_activity_dict_existing(self): - ''' test receiving a dict of an existing object in the db ''' + """ test receiving a dict of an existing object in the db """ instance = fields.ForeignKey(User, on_delete=models.CASCADE) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_user.json' - ) + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) user = User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') - user.remote_id = 'https://example.com/user/mouse' + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" + ) + user.remote_id = "https://example.com/user/mouse" user.save(broadcast=False) User.objects.create_user( - 'rat', 'rat@rat.rat', 'ratword', - local=True, localname='rat') + "rat", "rat@rat.rat", "ratword", local=True, localname="rat" + ) - with patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast'): + with patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast"): value = instance.field_from_activity(activitypub.Person(**userdata)) self.assertEqual(value, user) - def test_foreign_key_from_activity_str_existing(self): - ''' test receiving a remote id of an existing object in the db ''' + """ test receiving a remote id of an existing object in the db """ instance = fields.ForeignKey(User, on_delete=models.CASCADE) user = User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" + ) User.objects.create_user( - 'rat', 'rat@rat.rat', 'ratword', - local=True, localname='rat') + "rat", "rat@rat.rat", "ratword", local=True, localname="rat" + ) value = instance.field_from_activity(user.remote_id) self.assertEqual(value, user) - def test_one_to_one_field(self): - ''' a gussied up foreign key ''' - instance = fields.OneToOneField('User', on_delete=models.CASCADE) - Serializable = namedtuple('Serializable', ('to_activity', 'remote_id')) - item = Serializable(lambda: {'a': 'b'}, 'https://e.b/c') - self.assertEqual(instance.field_to_activity(item), {'a': 'b'}) + """ a gussied up foreign key """ + instance = fields.OneToOneField("User", on_delete=models.CASCADE) + Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) + item = Serializable(lambda: {"a": "b"}, "https://e.b/c") + self.assertEqual(instance.field_to_activity(item), {"a": "b"}) def test_many_to_many_field(self): - ''' lists! ''' - instance = fields.ManyToManyField('User') + """ lists! """ + instance = fields.ManyToManyField("User") - Serializable = namedtuple('Serializable', ('to_activity', 'remote_id')) - Queryset = namedtuple('Queryset', ('all', 'instance')) - item = Serializable(lambda: {'a': 'b'}, 'https://e.b/c') - another_item = Serializable(lambda: {}, 'example.com') + Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) + Queryset = namedtuple("Queryset", ("all", "instance")) + item = Serializable(lambda: {"a": "b"}, "https://e.b/c") + another_item = Serializable(lambda: {}, "example.com") items = Queryset(lambda: [item], another_item) - self.assertEqual(instance.field_to_activity(items), ['https://e.b/c']) + self.assertEqual(instance.field_to_activity(items), ["https://e.b/c"]) - instance = fields.ManyToManyField('User', link_only=True) - instance.name = 'snake_case' - self.assertEqual( - instance.field_to_activity(items), - 'example.com/snake_case' - ) + instance = fields.ManyToManyField("User", link_only=True) + instance.name = "snake_case" + self.assertEqual(instance.field_to_activity(items), "example.com/snake_case") @responses.activate def test_many_to_many_field_from_activity(self): - ''' resolve related fields for a list, takes a list of remote ids ''' + """ resolve related fields for a list, takes a list of remote ids """ instance = fields.ManyToManyField(User) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_user.json' - ) + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) # don't try to load the user icon - del userdata['icon'] + del userdata["icon"] # test receiving an unknown remote id and loading data responses.add( - responses.GET, - 'https://example.com/user/mouse', - json=userdata, - status=200) - with patch('bookwyrm.models.user.set_remote_server.delay'): + responses.GET, "https://example.com/user/mouse", json=userdata, status=200 + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): value = instance.field_from_activity( - ['https://example.com/user/mouse', 'bleh'] + ["https://example.com/user/mouse", "bleh"] ) self.assertIsInstance(value, list) self.assertEqual(len(value), 1) self.assertIsInstance(value[0], User) def test_tag_field(self): - ''' a special type of many to many field ''' - instance = fields.TagField('User') + """ a special type of many to many field """ + instance = fields.TagField("User") Serializable = namedtuple( - 'Serializable', - ('to_activity', 'remote_id', 'name_field', 'name') + "Serializable", ("to_activity", "remote_id", "name_field", "name") ) - Queryset = namedtuple('Queryset', ('all', 'instance')) - item = Serializable( - lambda: {'a': 'b'}, 'https://e.b/c', 'name', 'Name') - another_item = Serializable( - lambda: {}, 'example.com', '', '') + Queryset = namedtuple("Queryset", ("all", "instance")) + item = Serializable(lambda: {"a": "b"}, "https://e.b/c", "name", "Name") + another_item = Serializable(lambda: {}, "example.com", "", "") items = Queryset(lambda: [item], another_item) result = instance.field_to_activity(items) self.assertIsInstance(result, list) self.assertEqual(len(result), 1) - self.assertEqual(result[0].href, 'https://e.b/c') - self.assertEqual(result[0].name, 'Name') - self.assertEqual(result[0].type, 'Serializable') - + self.assertEqual(result[0].href, "https://e.b/c") + self.assertEqual(result[0].name, "Name") + self.assertEqual(result[0].type, "Serializable") def test_tag_field_from_activity(self): - ''' loadin' a list of items from Links ''' + """ loadin' a list of items from Links """ # TODO - @responses.activate - @patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast') + @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") def test_image_field(self, _): - ''' storing images ''' + """ storing images """ user = User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" + ) image_file = pathlib.Path(__file__).parent.joinpath( - '../../static/images/default_avi.jpg') + "../../static/images/default_avi.jpg" + ) image = Image.open(image_file) output = BytesIO() image.save(output, format=image.format) - user.avatar.save( - 'test.jpg', - ContentFile(output.getvalue()) - ) + user.avatar.save("test.jpg", ContentFile(output.getvalue())) - output = fields.image_serializer(user.avatar, alt='alt text') + output = fields.image_serializer(user.avatar, alt="alt text") self.assertIsNotNone( re.match( - r'.*\.jpg', + r".*\.jpg", output.url, ) ) - self.assertEqual(output.name, 'alt text') - self.assertEqual(output.type, 'Image') + self.assertEqual(output.name, "alt text") + self.assertEqual(output.type, "Image") instance = fields.ImageField() @@ -433,36 +413,30 @@ class ActivitypubFields(TestCase): responses.add( responses.GET, - 'http://www.example.com/image.jpg', + "http://www.example.com/image.jpg", body=user.avatar.file.read(), - status=200) - loaded_image = instance.field_from_activity( - 'http://www.example.com/image.jpg') + status=200, + ) + loaded_image = instance.field_from_activity("http://www.example.com/image.jpg") self.assertIsInstance(loaded_image, list) self.assertIsInstance(loaded_image[1], ContentFile) - def test_datetime_field(self): - ''' this one is pretty simple, it just has to use isoformat ''' + """ this one is pretty simple, it just has to use isoformat """ instance = fields.DateTimeField() now = timezone.now() self.assertEqual(instance.field_to_activity(now), now.isoformat()) - self.assertEqual( - instance.field_from_activity(now.isoformat()), now - ) - self.assertEqual(instance.field_from_activity('bip'), None) - + self.assertEqual(instance.field_from_activity(now.isoformat()), now) + self.assertEqual(instance.field_from_activity("bip"), None) def test_array_field(self): - ''' idk why it makes them strings but probably for a good reason ''' + """ idk why it makes them strings but probably for a good reason """ instance = fields.ArrayField(fields.IntegerField) - self.assertEqual(instance.field_to_activity([0, 1]), ['0', '1']) - + self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"]) def test_html_field(self): - ''' sanitizes html, the sanitizer has its own tests ''' + """ sanitizes html, the sanitizer has its own tests """ instance = fields.HtmlField() self.assertEqual( - instance.field_from_activity('

hi

'), - '

hi

' + instance.field_from_activity("

hi

"), "

hi

" ) diff --git a/bookwyrm/tests/models/test_import_model.py b/bookwyrm/tests/models/test_import_model.py index 7ec4e7003..38c3b1ed3 100644 --- a/bookwyrm/tests/models/test_import_model.py +++ b/bookwyrm/tests/models/test_import_model.py @@ -1,4 +1,4 @@ -''' testing models ''' +""" testing models """ import datetime import json import pathlib @@ -14,165 +14,166 @@ from bookwyrm.connectors.abstract_connector import SearchResult class ImportJob(TestCase): - ''' this is a fancy one!!! ''' + """ this is a fancy one!!! """ + def setUp(self): - ''' data is from a goodreads export of The Raven Tower ''' + """ data is from a goodreads export of The Raven Tower """ read_data = { - 'Book Id': 39395857, - 'Title': 'The Raven Tower', - 'Author': 'Ann Leckie', - 'Author l-f': 'Leckie, Ann', - 'Additional Authors': '', - 'ISBN': '="0356506991"', - 'ISBN13': '="9780356506999"', - 'My Rating': 0, - 'Average Rating': 4.06, - 'Publisher': 'Orbit', - 'Binding': 'Hardcover', - 'Number of Pages': 416, - 'Year Published': 2019, - 'Original Publication Year': 2019, - 'Date Read': '2019/04/12', - 'Date Added': '2019/04/09', - 'Bookshelves': '', - 'Bookshelves with positions': '', - 'Exclusive Shelf': 'read', - 'My Review': '', - 'Spoiler': '', - 'Private Notes': '', - 'Read Count': 1, - 'Recommended For': '', - 'Recommended By': '', - 'Owned Copies': 0, - 'Original Purchase Date': '', - 'Original Purchase Location': '', - 'Condition': '', - 'Condition Description': '', - 'BCID': '' + "Book Id": 39395857, + "Title": "The Raven Tower", + "Author": "Ann Leckie", + "Author l-f": "Leckie, Ann", + "Additional Authors": "", + "ISBN": '="0356506991"', + "ISBN13": '="9780356506999"', + "My Rating": 0, + "Average Rating": 4.06, + "Publisher": "Orbit", + "Binding": "Hardcover", + "Number of Pages": 416, + "Year Published": 2019, + "Original Publication Year": 2019, + "Date Read": "2019/04/12", + "Date Added": "2019/04/09", + "Bookshelves": "", + "Bookshelves with positions": "", + "Exclusive Shelf": "read", + "My Review": "", + "Spoiler": "", + "Private Notes": "", + "Read Count": 1, + "Recommended For": "", + "Recommended By": "", + "Owned Copies": 0, + "Original Purchase Date": "", + "Original Purchase Location": "", + "Condition": "", + "Condition Description": "", + "BCID": "", } currently_reading_data = read_data.copy() - currently_reading_data['Exclusive Shelf'] = 'currently-reading' - currently_reading_data['Date Read'] = '' + currently_reading_data["Exclusive Shelf"] = "currently-reading" + currently_reading_data["Date Read"] = "" unknown_read_data = currently_reading_data.copy() - unknown_read_data['Exclusive Shelf'] = 'read' - unknown_read_data['Date Read'] = '' + unknown_read_data["Exclusive Shelf"] = "read" + unknown_read_data["Date Read"] = "" user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" + ) job = models.ImportJob.objects.create(user=user) self.item_1 = models.ImportItem.objects.create( - job=job, index=1, data=currently_reading_data) - self.item_2 = models.ImportItem.objects.create( - job=job, index=2, data=read_data) + job=job, index=1, data=currently_reading_data + ) + self.item_2 = models.ImportItem.objects.create(job=job, index=2, data=read_data) self.item_3 = models.ImportItem.objects.create( - job=job, index=3, data=unknown_read_data) - + job=job, index=3, data=unknown_read_data + ) def test_isbn(self): - ''' it unquotes the isbn13 field from data ''' - expected = '9780356506999' + """ it unquotes the isbn13 field from data """ + expected = "9780356506999" item = models.ImportItem.objects.get(index=1) self.assertEqual(item.isbn, expected) - def test_shelf(self): - ''' converts to the local shelf typology ''' - expected = 'reading' + """ converts to the local shelf typology """ + expected = "reading" self.assertEqual(self.item_1.shelf, expected) - def test_date_added(self): - ''' converts to the local shelf typology ''' + """ converts to the local shelf typology """ expected = datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) item = models.ImportItem.objects.get(index=1) self.assertEqual(item.date_added, expected) - def test_date_read(self): - ''' converts to the local shelf typology ''' + """ converts to the local shelf typology """ expected = datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc) item = models.ImportItem.objects.get(index=2) self.assertEqual(item.date_read, expected) - def test_currently_reading_reads(self): - ''' infer currently reading dates where available ''' - expected = [models.ReadThrough( - start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) - )] + """ infer currently reading dates where available """ + expected = [ + models.ReadThrough( + start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) + ) + ] actual = models.ImportItem.objects.get(index=1) self.assertEqual(actual.reads[0].start_date, expected[0].start_date) self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date) def test_read_reads(self): - ''' infer read dates where available ''' + """ infer read dates where available """ actual = self.item_2 self.assertEqual( actual.reads[0].start_date, - datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc)) + datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc), + ) self.assertEqual( actual.reads[0].finish_date, - datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc)) + datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc), + ) def test_unread_reads(self): - ''' handle books with no read dates ''' + """ handle books with no read dates """ expected = [] actual = models.ImportItem.objects.get(index=3) self.assertEqual(actual.reads, expected) - @responses.activate def test_get_book_from_isbn(self): - ''' search and load books by isbn (9780356506999) ''' + """ search and load books by isbn (9780356506999) """ connector_info = models.Connector.objects.create( - identifier='openlibrary.org', - name='OpenLibrary', - connector_file='openlibrary', - base_url='https://openlibrary.org', - books_url='https://openlibrary.org', - covers_url='https://covers.openlibrary.org', - search_url='https://openlibrary.org/search?q=', + identifier="openlibrary.org", + name="OpenLibrary", + connector_file="openlibrary", + base_url="https://openlibrary.org", + books_url="https://openlibrary.org", + covers_url="https://covers.openlibrary.org", + search_url="https://openlibrary.org/search?q=", priority=3, ) connector = connector_manager.load_connector(connector_info) result = SearchResult( - title='Test Result', - key='https://openlibrary.org/works/OL1234W', - author='An Author', - year='1980', + title="Test Result", + key="https://openlibrary.org/works/OL1234W", + author="An Author", + year="1980", connector=connector, ) - - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ol_edition.json') + datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_edition.json") bookdata = json.loads(datafile.read_bytes()) responses.add( responses.GET, - 'https://openlibrary.org/works/OL1234W', + "https://openlibrary.org/works/OL1234W", json=bookdata, - status=200) + status=200, + ) responses.add( responses.GET, - 'https://openlibrary.org/works/OL15832982W', + "https://openlibrary.org/works/OL15832982W", json=bookdata, - status=200) + status=200, + ) responses.add( responses.GET, - 'https://openlibrary.org/authors/OL382982A', - json={'name': 'test author'}, - status=200) + "https://openlibrary.org/authors/OL382982A", + json={"name": "test author"}, + status=200, + ) - with patch( - 'bookwyrm.connectors.abstract_connector.load_more_data.delay'): + with patch("bookwyrm.connectors.abstract_connector.load_more_data.delay"): with patch( - 'bookwyrm.connectors.connector_manager.first_search_result' - ) as search: + "bookwyrm.connectors.connector_manager.first_search_result" + ) as search: search.return_value = result - with patch('bookwyrm.connectors.openlibrary.Connector.' \ - 'get_authors_from_data'): + with patch( + "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" + ): book = self.item_1.get_book_from_isbn() - self.assertEqual(book.title, 'Sabriel') + self.assertEqual(book.title, "Sabriel") diff --git a/bookwyrm/tests/models/test_list.py b/bookwyrm/tests/models/test_list.py index d41b81c76..6bc4b796b 100644 --- a/bookwyrm/tests/models/test_list.py +++ b/bookwyrm/tests/models/test_list.py @@ -1,43 +1,41 @@ -''' testing models ''' +""" testing models """ from unittest.mock import patch from django.test import TestCase from bookwyrm import models, settings -@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') +@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class List(TestCase): - ''' some activitypub oddness ahead ''' + """ some activitypub oddness ahead """ + def setUp(self): - ''' look, a list ''' + """ look, a list """ self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - self.list = models.List.objects.create( - name='Test List', user=self.user) + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" + ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + self.list = models.List.objects.create(name="Test List", user=self.user) def test_remote_id(self, _): - ''' shelves use custom remote ids ''' - expected_id = 'https://%s/list/%d' % \ - (settings.DOMAIN, self.list.id) + """ shelves use custom remote ids """ + expected_id = "https://%s/list/%d" % (settings.DOMAIN, self.list.id) self.assertEqual(self.list.get_remote_id(), expected_id) - def test_to_activity(self, _): - ''' jsonify it ''' + """ jsonify it """ activity_json = self.list.to_activity() self.assertIsInstance(activity_json, dict) - self.assertEqual(activity_json['id'], self.list.remote_id) - self.assertEqual(activity_json['totalItems'], 0) - self.assertEqual(activity_json['type'], 'BookList') - self.assertEqual(activity_json['name'], 'Test List') - self.assertEqual(activity_json['owner'], self.user.remote_id) + self.assertEqual(activity_json["id"], self.list.remote_id) + self.assertEqual(activity_json["totalItems"], 0) + self.assertEqual(activity_json["type"], "BookList") + self.assertEqual(activity_json["name"], "Test List") + self.assertEqual(activity_json["owner"], self.user.remote_id) def test_list_item(self, _): - ''' a list entry ''' - work = models.Work.objects.create(title='hello') - book = models.Edition.objects.create(title='hi', parent_work=work) + """ a list entry """ + work = models.Work.objects.create(title="hello") + book = models.Edition.objects.create(title="hi", parent_work=work) item = models.ListItem.objects.create( book_list=self.list, book=book, @@ -47,11 +45,11 @@ class List(TestCase): self.assertTrue(item.approved) add_activity = item.to_add_activity() - self.assertEqual(add_activity['actor'], self.user.remote_id) - self.assertEqual(add_activity['object']['id'], book.remote_id) - self.assertEqual(add_activity['target'], self.list.remote_id) + self.assertEqual(add_activity["actor"], self.user.remote_id) + self.assertEqual(add_activity["object"]["id"], book.remote_id) + self.assertEqual(add_activity["target"], self.list.remote_id) remove_activity = item.to_remove_activity() - self.assertEqual(remove_activity['actor'], self.user.remote_id) - self.assertEqual(remove_activity['object']['id'], book.remote_id) - self.assertEqual(remove_activity['target'], self.list.remote_id) + self.assertEqual(remove_activity["actor"], self.user.remote_id) + self.assertEqual(remove_activity["object"]["id"], book.remote_id) + self.assertEqual(remove_activity["target"], self.list.remote_id) diff --git a/bookwyrm/tests/models/test_readthrough_model.py b/bookwyrm/tests/models/test_readthrough_model.py index 3fcdf1e4a..f69e87790 100644 --- a/bookwyrm/tests/models/test_readthrough_model.py +++ b/bookwyrm/tests/models/test_readthrough_model.py @@ -1,4 +1,4 @@ -''' testing models ''' +""" testing models """ from django.test import TestCase from django.core.exceptions import ValidationError @@ -6,39 +6,36 @@ from bookwyrm import models, settings class ReadThrough(TestCase): - ''' some activitypub oddness ahead ''' - def setUp(self): - ''' look, a shelf ''' - self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') + """ some activitypub oddness ahead """ - self.work = models.Work.objects.create( - title='Example Work' + def setUp(self): + """ look, a shelf """ + self.user = models.User.objects.create_user( + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) + self.work = models.Work.objects.create(title="Example Work") + self.edition = models.Edition.objects.create( - title='Example Edition', - parent_work=self.work + title="Example Edition", parent_work=self.work ) self.work.default_edition = self.edition self.work.save() self.readthrough = models.ReadThrough.objects.create( - user=self.user, - book=self.edition) + user=self.user, book=self.edition + ) def test_progress_update(self): - ''' Test progress updates ''' - self.readthrough.create_update() # No-op, no progress yet + """ Test progress updates """ + self.readthrough.create_update() # No-op, no progress yet self.readthrough.progress = 10 self.readthrough.create_update() self.readthrough.progress = 20 self.readthrough.progress_mode = models.ProgressMode.PERCENT self.readthrough.create_update() - updates = self.readthrough.progressupdate_set \ - .order_by('created_date').all() + updates = self.readthrough.progressupdate_set.order_by("created_date").all() self.assertEqual(len(updates), 2) self.assertEqual(updates[0].progress, 10) self.assertEqual(updates[0].mode, models.ProgressMode.PAGE) diff --git a/bookwyrm/tests/models/test_relationship_models.py b/bookwyrm/tests/models/test_relationship_models.py index 0ef534502..0e842b218 100644 --- a/bookwyrm/tests/models/test_relationship_models.py +++ b/bookwyrm/tests/models/test_relationship_models.py @@ -1,4 +1,4 @@ -''' testing models ''' +""" testing models """ from unittest.mock import patch from django.test import TestCase @@ -6,87 +6,79 @@ from bookwyrm import models class Relationship(TestCase): - ''' following, blocking, stuff like that ''' + """ following, blocking, stuff like that """ + def setUp(self): - ''' we need some users for this ''' - with patch('bookwyrm.models.user.set_remote_server.delay'): + """ we need some users for this """ + with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'ratword', + "rat", + "rat@rat.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) self.local_user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse') - self.local_user.remote_id = 'http://local.com/user/mouse' + "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" + ) + self.local_user.remote_id = "http://local.com/user/mouse" self.local_user.save(broadcast=False) - def test_user_follows_from_request(self): - ''' convert a follow request into a follow ''' + """ convert a follow request into a follow """ real_broadcast = models.UserFollowRequest.broadcast + def mock_broadcast(_, activity, user): - ''' introspect what's being sent out ''' + """ introspect what's being sent out """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Follow') + self.assertEqual(activity["type"], "Follow") models.UserFollowRequest.broadcast = mock_broadcast request = models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user + user_subject=self.local_user, user_object=self.remote_user ) self.assertEqual( - request.remote_id, - 'http://local.com/user/mouse#follows/%d' % request.id + request.remote_id, "http://local.com/user/mouse#follows/%d" % request.id ) - self.assertEqual(request.status, 'follow_request') + self.assertEqual(request.status, "follow_request") rel = models.UserFollows.from_request(request) self.assertEqual( - rel.remote_id, - 'http://local.com/user/mouse#follows/%d' % request.id + rel.remote_id, "http://local.com/user/mouse#follows/%d" % request.id ) - self.assertEqual(rel.status, 'follows') + self.assertEqual(rel.status, "follows") self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_object, self.remote_user) models.UserFollowRequest.broadcast = real_broadcast - - def test_user_follows_from_request_custom_remote_id(self): - ''' store a specific remote id for a relationship provided by remote ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ store a specific remote id for a relationship provided by remote """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): request = models.UserFollowRequest.objects.create( user_subject=self.local_user, user_object=self.remote_user, - remote_id='http://antoher.server/sdkfhskdjf/23' + remote_id="http://antoher.server/sdkfhskdjf/23", ) - self.assertEqual( - request.remote_id, - 'http://antoher.server/sdkfhskdjf/23' - ) - self.assertEqual(request.status, 'follow_request') + self.assertEqual(request.remote_id, "http://antoher.server/sdkfhskdjf/23") + self.assertEqual(request.status, "follow_request") rel = models.UserFollows.from_request(request) - self.assertEqual( - rel.remote_id, - 'http://antoher.server/sdkfhskdjf/23' - ) - self.assertEqual(rel.status, 'follows') + self.assertEqual(rel.remote_id, "http://antoher.server/sdkfhskdjf/23") + self.assertEqual(rel.status, "follows") self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_object, self.remote_user) - def test_follow_request_activity(self): - ''' accept a request and make it a relationship ''' + """ accept a request and make it a relationship """ real_broadcast = models.UserFollowRequest.broadcast + def mock_broadcast(_, activity, user): self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['object'], self.remote_user.remote_id) - self.assertEqual(activity['type'], 'Follow') + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"], self.remote_user.remote_id) + self.assertEqual(activity["type"], "Follow") models.UserFollowRequest.broadcast = mock_broadcast models.UserFollowRequest.objects.create( @@ -95,15 +87,15 @@ class Relationship(TestCase): ) models.UserFollowRequest.broadcast = real_broadcast - def test_follow_request_accept(self): - ''' accept a request and make it a relationship ''' + """ accept a request and make it a relationship """ real_broadcast = models.UserFollowRequest.broadcast + def mock_broadcast(_, activity, user): self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Accept') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['object']['id'], 'https://www.hi.com/') + self.assertEqual(activity["type"], "Accept") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["id"], "https://www.hi.com/") self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) @@ -111,7 +103,7 @@ class Relationship(TestCase): request = models.UserFollowRequest.objects.create( user_subject=self.remote_user, user_object=self.local_user, - remote_id='https://www.hi.com/' + remote_id="https://www.hi.com/", ) request.accept() @@ -122,16 +114,15 @@ class Relationship(TestCase): self.assertEqual(rel.user_object, self.local_user) models.UserFollowRequest.broadcast = real_broadcast - def test_follow_request_reject(self): - ''' accept a request and make it a relationship ''' + """ accept a request and make it a relationship """ real_broadcast = models.UserFollowRequest.broadcast + def mock_reject(_, activity, user): self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Reject') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual( - activity['object']['id'], request.remote_id) + self.assertEqual(activity["type"], "Reject") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["id"], request.remote_id) models.UserFollowRequest.broadcast = mock_reject self.local_user.manually_approves_followers = True diff --git a/bookwyrm/tests/models/test_shelf_model.py b/bookwyrm/tests/models/test_shelf_model.py index c997326e7..3bbb9890d 100644 --- a/bookwyrm/tests/models/test_shelf_model.py +++ b/bookwyrm/tests/models/test_shelf_model.py @@ -1,116 +1,120 @@ -''' testing models ''' +""" testing models """ from django.test import TestCase from bookwyrm import models, settings -#pylint: disable=unused-argument +# pylint: disable=unused-argument class Shelf(TestCase): - ''' some activitypub oddness ahead ''' + """ some activitypub oddness ahead """ + def setUp(self): - ''' look, a shelf ''' + """ look, a shelf """ self.local_user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') - work = models.Work.objects.create(title='Test Work') - self.book = models.Edition.objects.create( - title='test book', - parent_work=work) + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" + ) + work = models.Work.objects.create(title="Test Work") + self.book = models.Edition.objects.create(title="test book", parent_work=work) def test_remote_id(self): - ''' shelves use custom remote ids ''' + """ shelves use custom remote ids """ real_broadcast = models.Shelf.broadcast + def broadcast_mock(_, activity, user, **kwargs): - ''' nah ''' + """ nah """ + models.Shelf.broadcast = broadcast_mock shelf = models.Shelf.objects.create( - name='Test Shelf', identifier='test-shelf', - user=self.local_user) - expected_id = 'https://%s/user/mouse/shelf/test-shelf' % settings.DOMAIN + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) + expected_id = "https://%s/user/mouse/shelf/test-shelf" % settings.DOMAIN self.assertEqual(shelf.get_remote_id(), expected_id) models.Shelf.broadcast = real_broadcast - def test_to_activity(self): - ''' jsonify it ''' + """ jsonify it """ real_broadcast = models.Shelf.broadcast + def empty_mock(_, activity, user, **kwargs): - ''' nah ''' + """ nah """ + models.Shelf.broadcast = empty_mock shelf = models.Shelf.objects.create( - name='Test Shelf', identifier='test-shelf', - user=self.local_user) + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) activity_json = shelf.to_activity() self.assertIsInstance(activity_json, dict) - self.assertEqual(activity_json['id'], shelf.remote_id) - self.assertEqual(activity_json['totalItems'], 0) - self.assertEqual(activity_json['type'], 'Shelf') - self.assertEqual(activity_json['name'], 'Test Shelf') - self.assertEqual(activity_json['owner'], self.local_user.remote_id) + self.assertEqual(activity_json["id"], shelf.remote_id) + self.assertEqual(activity_json["totalItems"], 0) + self.assertEqual(activity_json["type"], "Shelf") + self.assertEqual(activity_json["name"], "Test Shelf") + self.assertEqual(activity_json["owner"], self.local_user.remote_id) models.Shelf.broadcast = real_broadcast - def test_create_update_shelf(self): - ''' create and broadcast shelf creation ''' + """ create and broadcast shelf creation """ real_broadcast = models.Shelf.broadcast + def create_mock(_, activity, user, **kwargs): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Create') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['object']['name'], 'Test Shelf') + self.assertEqual(activity["type"], "Create") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["name"], "Test Shelf") + models.Shelf.broadcast = create_mock shelf = models.Shelf.objects.create( - name='Test Shelf', identifier='test-shelf', user=self.local_user) + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) def update_mock(_, activity, user, **kwargs): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Update') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['object']['name'], 'arthur russel') + self.assertEqual(activity["type"], "Update") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["name"], "arthur russel") + models.Shelf.broadcast = update_mock - shelf.name = 'arthur russel' + shelf.name = "arthur russel" shelf.save() - self.assertEqual(shelf.name, 'arthur russel') + self.assertEqual(shelf.name, "arthur russel") models.Shelf.broadcast = real_broadcast - def test_shelve(self): - ''' create and broadcast shelf creation ''' + """ create and broadcast shelf creation """ real_broadcast = models.Shelf.broadcast real_shelfbook_broadcast = models.ShelfBook.broadcast def add_mock(_, activity, user, **kwargs): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Add') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['object']['id'], self.book.remote_id) - self.assertEqual(activity['target'], shelf.remote_id) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["id"], self.book.remote_id) + self.assertEqual(activity["target"], shelf.remote_id) def remove_mock(_, activity, user, **kwargs): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Remove') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['object']['id'], self.book.remote_id) - self.assertEqual(activity['target'], shelf.remote_id) + self.assertEqual(activity["type"], "Remove") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["id"], self.book.remote_id) + self.assertEqual(activity["target"], shelf.remote_id) def empty_mock(_, activity, user, **kwargs): - ''' nah ''' + """ nah """ models.Shelf.broadcast = empty_mock shelf = models.Shelf.objects.create( - name='Test Shelf', identifier='test-shelf', user=self.local_user) + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) models.ShelfBook.broadcast = add_mock shelf_book = models.ShelfBook.objects.create( - shelf=shelf, - user=self.local_user, - book=self.book) + shelf=shelf, user=self.local_user, book=self.book + ) self.assertEqual(shelf.books.first(), self.book) models.ShelfBook.broadcast = remove_mock diff --git a/bookwyrm/tests/models/test_status_model.py b/bookwyrm/tests/models/test_status_model.py index 29be5c072..21982c200 100644 --- a/bookwyrm/tests/models/test_status_model.py +++ b/bookwyrm/tests/models/test_status_model.py @@ -1,4 +1,4 @@ -''' testing models ''' +""" testing models """ from unittest.mock import patch from io import BytesIO import pathlib @@ -12,44 +12,45 @@ from django.utils import timezone from bookwyrm import models, settings -@patch('bookwyrm.models.Status.broadcast') +@patch("bookwyrm.models.Status.broadcast") class Status(TestCase): - ''' lotta types of statuses ''' + """ lotta types of statuses """ + def setUp(self): - ''' useful things for creating a status ''' + """ useful things for creating a status """ self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') - self.book = models.Edition.objects.create(title='Test Edition') + "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" + ) + self.book = models.Edition.objects.create(title="Test Edition") image_file = pathlib.Path(__file__).parent.joinpath( - '../../static/images/default_avi.jpg') + "../../static/images/default_avi.jpg" + ) image = Image.open(image_file) output = BytesIO() - with patch('bookwyrm.models.Status.broadcast'): + with patch("bookwyrm.models.Status.broadcast"): image.save(output, format=image.format) - self.book.cover.save( - 'test.jpg', - ContentFile(output.getvalue()) - ) + self.book.cover.save("test.jpg", ContentFile(output.getvalue())) def test_status_generated_fields(self, _): - ''' setting remote id ''' - status = models.Status.objects.create(content='bleh', user=self.user) - expected_id = 'https://%s/user/mouse/status/%d' % \ - (settings.DOMAIN, status.id) + """ setting remote id """ + status = models.Status.objects.create(content="bleh", user=self.user) + expected_id = "https://%s/user/mouse/status/%d" % (settings.DOMAIN, status.id) self.assertEqual(status.remote_id, expected_id) - self.assertEqual(status.privacy, 'public') + self.assertEqual(status.privacy, "public") def test_replies(self, _): - ''' get a list of replies ''' - parent = models.Status.objects.create(content='hi', user=self.user) + """ get a list of replies """ + parent = models.Status.objects.create(content="hi", user=self.user) child = models.Status.objects.create( - content='hello', reply_parent=parent, user=self.user) + content="hello", reply_parent=parent, user=self.user + ) models.Review.objects.create( - content='hey', reply_parent=parent, user=self.user, book=self.book) + content="hey", reply_parent=parent, user=self.user, book=self.book + ) models.Status.objects.create( - content='hi hello', reply_parent=child, user=self.user) + content="hi hello", reply_parent=child, user=self.user + ) replies = models.Status.replies(parent) self.assertEqual(replies.count(), 2) @@ -58,199 +59,228 @@ class Status(TestCase): self.assertIsInstance(replies.last(), models.Review) def test_status_type(self, _): - ''' class name ''' - self.assertEqual(models.Status().status_type, 'Note') - self.assertEqual(models.Review().status_type, 'Review') - self.assertEqual(models.Quotation().status_type, 'Quotation') - self.assertEqual(models.Comment().status_type, 'Comment') - self.assertEqual(models.Boost().status_type, 'Announce') + """ class name """ + self.assertEqual(models.Status().status_type, "Note") + self.assertEqual(models.Review().status_type, "Review") + self.assertEqual(models.Quotation().status_type, "Quotation") + self.assertEqual(models.Comment().status_type, "Comment") + self.assertEqual(models.Boost().status_type, "Announce") def test_boostable(self, _): - ''' can a status be boosted, based on privacy ''' - self.assertTrue(models.Status(privacy='public').boostable) - self.assertTrue(models.Status(privacy='unlisted').boostable) - self.assertFalse(models.Status(privacy='followers').boostable) - self.assertFalse(models.Status(privacy='direct').boostable) + """ can a status be boosted, based on privacy """ + self.assertTrue(models.Status(privacy="public").boostable) + self.assertTrue(models.Status(privacy="unlisted").boostable) + self.assertFalse(models.Status(privacy="followers").boostable) + self.assertFalse(models.Status(privacy="direct").boostable) def test_to_replies(self, _): - ''' activitypub replies collection ''' - parent = models.Status.objects.create(content='hi', user=self.user) + """ activitypub replies collection """ + parent = models.Status.objects.create(content="hi", user=self.user) child = models.Status.objects.create( - content='hello', reply_parent=parent, user=self.user) + content="hello", reply_parent=parent, user=self.user + ) models.Review.objects.create( - content='hey', reply_parent=parent, user=self.user, book=self.book) + content="hey", reply_parent=parent, user=self.user, book=self.book + ) models.Status.objects.create( - content='hi hello', reply_parent=child, user=self.user) + content="hi hello", reply_parent=child, user=self.user + ) replies = parent.to_replies() - self.assertEqual(replies['id'], '%s/replies' % parent.remote_id) - self.assertEqual(replies['totalItems'], 2) + self.assertEqual(replies["id"], "%s/replies" % parent.remote_id) + self.assertEqual(replies["totalItems"], 2) def test_status_to_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' - status = models.Status.objects.create( - content='test content', user=self.user) + """ subclass of the base model version with a "pure" serializer """ + status = models.Status.objects.create(content="test content", user=self.user) activity = status.to_activity() - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Note') - self.assertEqual(activity['content'], 'test content') - self.assertEqual(activity['sensitive'], False) + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Note") + self.assertEqual(activity["content"], "test content") + self.assertEqual(activity["sensitive"], False) def test_status_to_activity_tombstone(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.Status.objects.create( - content='test content', user=self.user, - deleted=True, deleted_date=timezone.now()) + content="test content", + user=self.user, + deleted=True, + deleted_date=timezone.now(), + ) activity = status.to_activity() - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Tombstone') - self.assertFalse(hasattr(activity, 'content')) + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Tombstone") + self.assertFalse(hasattr(activity, "content")) def test_status_to_pure_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' - status = models.Status.objects.create( - content='test content', user=self.user) + """ subclass of the base model version with a "pure" serializer """ + status = models.Status.objects.create(content="test content", user=self.user) activity = status.to_activity(pure=True) - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Note') - self.assertEqual(activity['content'], 'test content') - self.assertEqual(activity['sensitive'], False) - self.assertEqual(activity['attachment'], []) + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Note") + self.assertEqual(activity["content"], "test content") + self.assertEqual(activity["sensitive"], False) + self.assertEqual(activity["attachment"], []) def test_generated_note_to_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.GeneratedNote.objects.create( - content='test content', user=self.user) + content="test content", user=self.user + ) status.mention_books.set([self.book]) status.mention_users.set([self.user]) activity = status.to_activity() - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'GeneratedNote') - self.assertEqual(activity['content'], 'test content') - self.assertEqual(activity['sensitive'], False) - self.assertEqual(len(activity['tag']), 2) + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "GeneratedNote") + self.assertEqual(activity["content"], "test content") + self.assertEqual(activity["sensitive"], False) + self.assertEqual(len(activity["tag"]), 2) def test_generated_note_to_pure_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.GeneratedNote.objects.create( - content='test content', user=self.user) + content="test content", user=self.user + ) status.mention_books.set([self.book]) status.mention_users.set([self.user]) activity = status.to_activity(pure=True) - self.assertEqual(activity['id'], status.remote_id) + self.assertEqual(activity["id"], status.remote_id) self.assertEqual( - activity['content'], - 'mouse test content "Test Edition"' % \ - self.book.remote_id) - self.assertEqual(len(activity['tag']), 2) - self.assertEqual(activity['type'], 'Note') - self.assertEqual(activity['sensitive'], False) - self.assertIsInstance(activity['attachment'], list) - self.assertEqual(activity['attachment'][0].type, 'Image') - self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \ - (settings.DOMAIN, self.book.cover.url)) + activity["content"], + 'mouse test content "Test Edition"' % self.book.remote_id, + ) + self.assertEqual(len(activity["tag"]), 2) + self.assertEqual(activity["type"], "Note") + self.assertEqual(activity["sensitive"], False) + self.assertIsInstance(activity["attachment"], list) + self.assertEqual(activity["attachment"][0].type, "Image") self.assertEqual( - activity['attachment'][0].name, 'Test Edition') + activity["attachment"][0].url, + "https://%s%s" % (settings.DOMAIN, self.book.cover.url), + ) + self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_comment_to_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.Comment.objects.create( - content='test content', user=self.user, book=self.book) + content="test content", user=self.user, book=self.book + ) activity = status.to_activity() - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Comment') - self.assertEqual(activity['content'], 'test content') - self.assertEqual(activity['inReplyToBook'], self.book.remote_id) + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Comment") + self.assertEqual(activity["content"], "test content") + self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_comment_to_pure_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.Comment.objects.create( - content='test content', user=self.user, book=self.book) + content="test content", user=self.user, book=self.book + ) activity = status.to_activity(pure=True) - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Note') + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Note") self.assertEqual( - activity['content'], - 'test content

(comment on "Test Edition")

' % - self.book.remote_id) - self.assertEqual(activity['attachment'][0].type, 'Image') - self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \ - (settings.DOMAIN, self.book.cover.url)) + activity["content"], + 'test content

(comment on "Test Edition")

' + % self.book.remote_id, + ) + self.assertEqual(activity["attachment"][0].type, "Image") self.assertEqual( - activity['attachment'][0].name, 'Test Edition') + activity["attachment"][0].url, + "https://%s%s" % (settings.DOMAIN, self.book.cover.url), + ) + self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_quotation_to_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.Quotation.objects.create( - quote='a sickening sense', content='test content', - user=self.user, book=self.book) + quote="a sickening sense", + content="test content", + user=self.user, + book=self.book, + ) activity = status.to_activity() - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Quotation') - self.assertEqual(activity['quote'], 'a sickening sense') - self.assertEqual(activity['content'], 'test content') - self.assertEqual(activity['inReplyToBook'], self.book.remote_id) + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Quotation") + self.assertEqual(activity["quote"], "a sickening sense") + self.assertEqual(activity["content"], "test content") + self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_quotation_to_pure_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.Quotation.objects.create( - quote='a sickening sense', content='test content', - user=self.user, book=self.book) + quote="a sickening sense", + content="test content", + user=self.user, + book=self.book, + ) activity = status.to_activity(pure=True) - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Note') + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Note") self.assertEqual( - activity['content'], - 'a sickening sense

-- "Test Edition"

' \ - 'test content' % self.book.remote_id) - self.assertEqual(activity['attachment'][0].type, 'Image') - self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \ - (settings.DOMAIN, self.book.cover.url)) + activity["content"], + 'a sickening sense

-- "Test Edition"

' + "test content" % self.book.remote_id, + ) + self.assertEqual(activity["attachment"][0].type, "Image") self.assertEqual( - activity['attachment'][0].name, 'Test Edition') + activity["attachment"][0].url, + "https://%s%s" % (settings.DOMAIN, self.book.cover.url), + ) + self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_review_to_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.Review.objects.create( - name='Review name', content='test content', rating=3, - user=self.user, book=self.book) + name="Review name", + content="test content", + rating=3, + user=self.user, + book=self.book, + ) activity = status.to_activity() - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Review') - self.assertEqual(activity['rating'], 3) - self.assertEqual(activity['name'], 'Review name') - self.assertEqual(activity['content'], 'test content') - self.assertEqual(activity['inReplyToBook'], self.book.remote_id) + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Review") + self.assertEqual(activity["rating"], 3) + self.assertEqual(activity["name"], "Review name") + self.assertEqual(activity["content"], "test content") + self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_review_to_pure_activity(self, _): - ''' subclass of the base model version with a "pure" serializer ''' + """ subclass of the base model version with a "pure" serializer """ status = models.Review.objects.create( - name='Review name', content='test content', rating=3, - user=self.user, book=self.book) + name="Review name", + content="test content", + rating=3, + user=self.user, + book=self.book, + ) activity = status.to_activity(pure=True) - self.assertEqual(activity['id'], status.remote_id) - self.assertEqual(activity['type'], 'Article') + self.assertEqual(activity["id"], status.remote_id) + self.assertEqual(activity["type"], "Article") self.assertEqual( - activity['name'], 'Review of "%s" (3 stars): Review name' \ - % self.book.title) - self.assertEqual(activity['content'], 'test content') - self.assertEqual(activity['attachment'][0].type, 'Image') - self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \ - (settings.DOMAIN, self.book.cover.url)) + activity["name"], 'Review of "%s" (3 stars): Review name' % self.book.title + ) + self.assertEqual(activity["content"], "test content") + self.assertEqual(activity["attachment"][0].type, "Image") self.assertEqual( - activity['attachment'][0].name, 'Test Edition') + activity["attachment"][0].url, + "https://%s%s" % (settings.DOMAIN, self.book.cover.url), + ) + self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_favorite(self, _): - ''' fav a status ''' + """ fav a status """ real_broadcast = models.Favorite.broadcast + def fav_broadcast_mock(_, activity, user): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.user.remote_id) - self.assertEqual(activity['type'], 'Like') + self.assertEqual(activity["type"], "Like") + models.Favorite.broadcast = fav_broadcast_mock - status = models.Status.objects.create( - content='test content', user=self.user) + status = models.Status.objects.create(content="test content", user=self.user) fav = models.Favorite.objects.create(status=status, user=self.user) # can't fav a status twice @@ -258,50 +288,47 @@ class Status(TestCase): models.Favorite.objects.create(status=status, user=self.user) activity = fav.to_activity() - self.assertEqual(activity['type'], 'Like') - self.assertEqual(activity['actor'], self.user.remote_id) - self.assertEqual(activity['object'], status.remote_id) + self.assertEqual(activity["type"], "Like") + self.assertEqual(activity["actor"], self.user.remote_id) + self.assertEqual(activity["object"], status.remote_id) models.Favorite.broadcast = real_broadcast def test_boost(self, _): - ''' boosting, this one's a bit fussy ''' - status = models.Status.objects.create( - content='test content', user=self.user) - boost = models.Boost.objects.create( - boosted_status=status, user=self.user) + """ boosting, this one's a bit fussy """ + status = models.Status.objects.create(content="test content", user=self.user) + boost = models.Boost.objects.create(boosted_status=status, user=self.user) activity = boost.to_activity() - self.assertEqual(activity['actor'], self.user.remote_id) - self.assertEqual(activity['object'], status.remote_id) - self.assertEqual(activity['type'], 'Announce') + self.assertEqual(activity["actor"], self.user.remote_id) + self.assertEqual(activity["object"], status.remote_id) + self.assertEqual(activity["type"], "Announce") self.assertEqual(activity, boost.to_activity(pure=True)) def test_notification(self, _): - ''' a simple model ''' + """ a simple model """ notification = models.Notification.objects.create( - user=self.user, notification_type='FAVORITE') + user=self.user, notification_type="FAVORITE" + ) self.assertFalse(notification.read) with self.assertRaises(IntegrityError): models.Notification.objects.create( - user=self.user, notification_type='GLORB') - + user=self.user, notification_type="GLORB" + ) def test_create_broadcast(self, broadcast_mock): - ''' should send out two verions of a status on create ''' - models.Comment.objects.create( - content='hi', user=self.user, book=self.book) + """ should send out two verions of a status on create """ + models.Comment.objects.create(content="hi", user=self.user, book=self.book) self.assertEqual(broadcast_mock.call_count, 2) pure_call = broadcast_mock.call_args_list[0] bw_call = broadcast_mock.call_args_list[1] - self.assertEqual(pure_call[1]['software'], 'other') + self.assertEqual(pure_call[1]["software"], "other") args = pure_call[0][0] - self.assertEqual(args['type'], 'Create') - self.assertEqual(args['object']['type'], 'Note') - self.assertTrue('content' in args['object']) + self.assertEqual(args["type"], "Create") + self.assertEqual(args["object"]["type"], "Note") + self.assertTrue("content" in args["object"]) - - self.assertEqual(bw_call[1]['software'], 'bookwyrm') + self.assertEqual(bw_call[1]["software"], "bookwyrm") args = bw_call[0][0] - self.assertEqual(args['type'], 'Create') - self.assertEqual(args['object']['type'], 'Comment') + self.assertEqual(args["type"], "Create") + self.assertEqual(args["object"]["type"], "Comment") diff --git a/bookwyrm/tests/models/test_user_model.py b/bookwyrm/tests/models/test_user_model.py index ed3ad41a5..618e4f781 100644 --- a/bookwyrm/tests/models/test_user_model.py +++ b/bookwyrm/tests/models/test_user_model.py @@ -1,4 +1,4 @@ -''' testing models ''' +""" testing models """ from unittest.mock import patch from django.test import TestCase import responses @@ -11,75 +11,84 @@ from bookwyrm.settings import DOMAIN class User(TestCase): def setUp(self): self.user = models.User.objects.create_user( - 'mouse@%s' % DOMAIN, 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse', name='hi', bookwyrm_user=False) + "mouse@%s" % DOMAIN, + "mouse@mouse.mouse", + "mouseword", + local=True, + localname="mouse", + name="hi", + bookwyrm_user=False, + ) def test_computed_fields(self): - ''' username instead of id here ''' - expected_id = 'https://%s/user/mouse' % DOMAIN + """ username instead of id here """ + expected_id = "https://%s/user/mouse" % DOMAIN self.assertEqual(self.user.remote_id, expected_id) - self.assertEqual(self.user.username, 'mouse@%s' % DOMAIN) - self.assertEqual(self.user.localname, 'mouse') - self.assertEqual(self.user.shared_inbox, 'https://%s/inbox' % DOMAIN) - self.assertEqual(self.user.inbox, '%s/inbox' % expected_id) - self.assertEqual(self.user.outbox, '%s/outbox' % expected_id) + self.assertEqual(self.user.username, "mouse@%s" % DOMAIN) + self.assertEqual(self.user.localname, "mouse") + self.assertEqual(self.user.shared_inbox, "https://%s/inbox" % DOMAIN) + self.assertEqual(self.user.inbox, "%s/inbox" % expected_id) + self.assertEqual(self.user.outbox, "%s/outbox" % expected_id) self.assertIsNotNone(self.user.key_pair.private_key) self.assertIsNotNone(self.user.key_pair.public_key) def test_remote_user(self): - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): user = models.User.objects.create_user( - 'rat', 'rat@rat.rat', 'ratword', local=False, - remote_id='https://example.com/dfjkg', bookwyrm_user=False) - self.assertEqual(user.username, 'rat@example.com') - + "rat", + "rat@rat.rat", + "ratword", + local=False, + remote_id="https://example.com/dfjkg", + bookwyrm_user=False, + ) + self.assertEqual(user.username, "rat@example.com") def test_user_shelves(self): shelves = models.Shelf.objects.filter(user=self.user).all() self.assertEqual(len(shelves), 3) names = [s.name for s in shelves] - self.assertTrue('To Read' in names) - self.assertTrue('Currently Reading' in names) - self.assertTrue('Read' in names) + self.assertTrue("To Read" in names) + self.assertTrue("Currently Reading" in names) + self.assertTrue("Read" in names) ids = [s.identifier for s in shelves] - self.assertTrue('to-read' in ids) - self.assertTrue('reading' in ids) - self.assertTrue('read' in ids) - + self.assertTrue("to-read" in ids) + self.assertTrue("reading" in ids) + self.assertTrue("read" in ids) def test_activitypub_serialize(self): activity = self.user.to_activity() - self.assertEqual(activity['id'], self.user.remote_id) - self.assertEqual(activity['@context'], [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - { - 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers', - 'schema': 'http://schema.org#', - 'PropertyValue': 'schema:PropertyValue', - 'value': 'schema:value', - } - ]) - self.assertEqual(activity['preferredUsername'], self.user.localname) - self.assertEqual(activity['name'], self.user.name) - self.assertEqual(activity['inbox'], self.user.inbox) - self.assertEqual(activity['outbox'], self.user.outbox) - self.assertEqual(activity['bookwyrmUser'], False) - self.assertEqual(activity['discoverable'], True) - self.assertEqual(activity['type'], 'Person') + self.assertEqual(activity["id"], self.user.remote_id) + self.assertEqual( + activity["@context"], + [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + }, + ], + ) + self.assertEqual(activity["preferredUsername"], self.user.localname) + self.assertEqual(activity["name"], self.user.name) + self.assertEqual(activity["inbox"], self.user.inbox) + self.assertEqual(activity["outbox"], self.user.outbox) + self.assertEqual(activity["bookwyrmUser"], False) + self.assertEqual(activity["discoverable"], True) + self.assertEqual(activity["type"], "Person") def test_activitypub_outbox(self): activity = self.user.to_outbox() - self.assertEqual(activity['type'], 'OrderedCollection') - self.assertEqual(activity['id'], self.user.outbox) - self.assertEqual(activity['totalItems'], 0) - + self.assertEqual(activity["type"], "OrderedCollection") + self.assertEqual(activity["id"], self.user.outbox) + self.assertEqual(activity["totalItems"], 0) def test_set_remote_server(self): server = models.FederatedServer.objects.create( - server_name=DOMAIN, - application_type='test type', - application_version=3 + server_name=DOMAIN, application_type="test type", application_version=3 ) models.user.set_remote_server(self.user.id) @@ -91,26 +100,24 @@ class User(TestCase): def test_get_or_create_remote_server(self): responses.add( responses.GET, - 'https://%s/.well-known/nodeinfo' % DOMAIN, - json={'links': [{'href': 'http://www.example.com'}, {}]} + "https://%s/.well-known/nodeinfo" % DOMAIN, + json={"links": [{"href": "http://www.example.com"}, {}]}, ) responses.add( responses.GET, - 'http://www.example.com', - json={'software': {'name': 'hi', 'version': '2'}}, + "http://www.example.com", + json={"software": {"name": "hi", "version": "2"}}, ) server = models.user.get_or_create_remote_server(DOMAIN) self.assertEqual(server.server_name, DOMAIN) - self.assertEqual(server.application_type, 'hi') - self.assertEqual(server.application_version, '2') + self.assertEqual(server.application_type, "hi") + self.assertEqual(server.application_version, "2") @responses.activate def test_get_or_create_remote_server_no_wellknown(self): responses.add( - responses.GET, - 'https://%s/.well-known/nodeinfo' % DOMAIN, - status=404 + responses.GET, "https://%s/.well-known/nodeinfo" % DOMAIN, status=404 ) server = models.user.get_or_create_remote_server(DOMAIN) @@ -122,14 +129,10 @@ class User(TestCase): def test_get_or_create_remote_server_no_links(self): responses.add( responses.GET, - 'https://%s/.well-known/nodeinfo' % DOMAIN, - json={'links': [{'href': 'http://www.example.com'}, {}]} - ) - responses.add( - responses.GET, - 'http://www.example.com', - status=404 + "https://%s/.well-known/nodeinfo" % DOMAIN, + json={"links": [{"href": "http://www.example.com"}, {}]}, ) + responses.add(responses.GET, "http://www.example.com", status=404) server = models.user.get_or_create_remote_server(DOMAIN) self.assertEqual(server.server_name, DOMAIN) @@ -140,14 +143,10 @@ class User(TestCase): def test_get_or_create_remote_server_unknown_format(self): responses.add( responses.GET, - 'https://%s/.well-known/nodeinfo' % DOMAIN, - json={'links': [{'href': 'http://www.example.com'}, {}]} - ) - responses.add( - responses.GET, - 'http://www.example.com', - json={'fish': 'salmon'} + "https://%s/.well-known/nodeinfo" % DOMAIN, + json={"links": [{"href": "http://www.example.com"}, {}]}, ) + responses.add(responses.GET, "http://www.example.com", json={"fish": "salmon"}) server = models.user.get_or_create_remote_server(DOMAIN) self.assertEqual(server.server_name, DOMAIN) diff --git a/bookwyrm/tests/test_goodreads_import.py b/bookwyrm/tests/test_goodreads_import.py index aee9afe49..080ccd15b 100644 --- a/bookwyrm/tests/test_goodreads_import.py +++ b/bookwyrm/tests/test_goodreads_import.py @@ -1,4 +1,4 @@ -''' testing import ''' +""" testing import """ from collections import namedtuple import csv import pathlib @@ -12,125 +12,117 @@ from bookwyrm.goodreads_import import GoodreadsImporter from bookwyrm import importer from bookwyrm.settings import DOMAIN + class GoodreadsImport(TestCase): - ''' importing from goodreads csv ''' + """ importing from goodreads csv """ + def setUp(self): self.importer = GoodreadsImporter() - ''' use a test csv ''' - datafile = pathlib.Path(__file__).parent.joinpath( - 'data/goodreads.csv') - self.csv = open(datafile, 'r', encoding=self.importer.encoding) + """ use a test csv """ + datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv") + self.csv = open(datafile, "r", encoding=self.importer.encoding) self.user = models.User.objects.create_user( - 'mouse', 'mouse@mouse.mouse', 'password', local=True) + "mouse", "mouse@mouse.mouse", "password", local=True + ) models.Connector.objects.create( identifier=DOMAIN, - name='Local', + name="Local", local=True, - connector_file='self_connector', - base_url='https://%s' % DOMAIN, - books_url='https://%s/book' % DOMAIN, - covers_url='https://%s/images/covers' % DOMAIN, - search_url='https://%s/search?q=' % DOMAIN, + connector_file="self_connector", + base_url="https://%s" % DOMAIN, + books_url="https://%s/book" % DOMAIN, + covers_url="https://%s/images/covers" % DOMAIN, + search_url="https://%s/search?q=" % DOMAIN, priority=1, ) - work = models.Work.objects.create(title='Test Work') + work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=work, ) - def test_create_job(self): - ''' creates the import job entry and checks csv ''' - import_job = self.importer.create_job( - self.user, self.csv, False, 'public') + """ creates the import job entry and checks csv """ + import_job = self.importer.create_job(self.user, self.csv, False, "public") self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.include_reviews, False) - self.assertEqual(import_job.privacy, 'public') + self.assertEqual(import_job.privacy, "public") import_items = models.ImportItem.objects.filter(job=import_job).all() self.assertEqual(len(import_items), 3) self.assertEqual(import_items[0].index, 0) - self.assertEqual(import_items[0].data['Book Id'], '42036538') + self.assertEqual(import_items[0].data["Book Id"], "42036538") self.assertEqual(import_items[1].index, 1) - self.assertEqual(import_items[1].data['Book Id'], '52691223') + self.assertEqual(import_items[1].data["Book Id"], "52691223") self.assertEqual(import_items[2].index, 2) - self.assertEqual(import_items[2].data['Book Id'], '28694510') - + self.assertEqual(import_items[2].data["Book Id"], "28694510") def test_create_retry_job(self): - ''' trying again with items that didn't import ''' - import_job = self.importer.create_job( - self.user, self.csv, False, 'unlisted') - import_items = models.ImportItem.objects.filter( - job=import_job - ).all()[:2] + """ trying again with items that didn't import """ + import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") + import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] - retry = self.importer.create_retry_job( - self.user, import_job, import_items) + retry = self.importer.create_retry_job(self.user, import_job, import_items) self.assertNotEqual(import_job, retry) self.assertEqual(retry.user, self.user) self.assertEqual(retry.include_reviews, False) - self.assertEqual(retry.privacy, 'unlisted') + self.assertEqual(retry.privacy, "unlisted") retry_items = models.ImportItem.objects.filter(job=retry).all() self.assertEqual(len(retry_items), 2) self.assertEqual(retry_items[0].index, 0) - self.assertEqual(retry_items[0].data['Book Id'], '42036538') + self.assertEqual(retry_items[0].data["Book Id"], "42036538") self.assertEqual(retry_items[1].index, 1) - self.assertEqual(retry_items[1].data['Book Id'], '52691223') - + self.assertEqual(retry_items[1].data["Book Id"], "52691223") def test_start_import(self): - ''' begin loading books ''' - import_job = self.importer.create_job( - self.user, self.csv, False, 'unlisted') - MockTask = namedtuple('Task', ('id')) + """ begin loading books """ + import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") + MockTask = namedtuple("Task", ("id")) mock_task = MockTask(7) - with patch('bookwyrm.importer.import_data.delay') as start: + with patch("bookwyrm.importer.import_data.delay") as start: start.return_value = mock_task self.importer.start_import(import_job) import_job.refresh_from_db() - self.assertEqual(import_job.task_id, '7') - + self.assertEqual(import_job.task_id, "7") @responses.activate def test_import_data(self): - ''' resolve entry ''' - import_job = self.importer.create_job( - self.user, self.csv, False, 'unlisted') - book = models.Edition.objects.create(title='Test Book') + """ resolve entry """ + import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") + book = models.Edition.objects.create(title="Test Book") with patch( - 'bookwyrm.models.import_job.ImportItem.get_book_from_isbn' - ) as resolve: + "bookwyrm.models.import_job.ImportItem.get_book_from_isbn" + ) as resolve: resolve.return_value = book - with patch('bookwyrm.importer.handle_imported_book'): + with patch("bookwyrm.importer.handle_imported_book"): importer.import_data(self.importer.service, import_job.id) import_item = models.ImportItem.objects.get(job=import_job, index=0) self.assertEqual(import_item.book.id, book.id) - def test_handle_imported_book(self): - ''' goodreads import added a book, this adds related connections ''' - shelf = self.user.shelf_set.filter(identifier='read').first() + """ goodreads import added a book, this adds related connections """ + shelf = self.user.shelf_set.filter(identifier="read").first() self.assertIsNone(shelf.books.first()) import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv') - csv_file = open(datafile, 'r') + datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv") + csv_file = open(datafile, "r") for index, entry in enumerate(list(csv.DictReader(csv_file))): entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=index, data=entry, book=self.book) + job_id=import_job.id, index=index, data=entry, book=self.book + ) break - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'public') + self.importer.service, self.user, import_item, False, "public" + ) shelf.refresh_from_db() self.assertEqual(shelf.books.first(), self.book) @@ -145,31 +137,30 @@ class GoodreadsImport(TestCase): self.assertEqual(readthrough.finish_date.month, 10) self.assertEqual(readthrough.finish_date.day, 25) - def test_handle_imported_book_already_shelved(self): - ''' goodreads import added a book, this adds related connections ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - shelf = self.user.shelf_set.filter(identifier='to-read').first() - models.ShelfBook.objects.create( - shelf=shelf, user=self.user, book=self.book) + """ goodreads import added a book, this adds related connections """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + shelf = self.user.shelf_set.filter(identifier="to-read").first() + models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv') - csv_file = open(datafile, 'r') + datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv") + csv_file = open(datafile, "r") for index, entry in enumerate(list(csv.DictReader(csv_file))): entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=index, data=entry, book=self.book) + job_id=import_job.id, index=index, data=entry, book=self.book + ) break - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'public') + self.importer.service, self.user, import_item, False, "public" + ) shelf.refresh_from_db() self.assertEqual(shelf.books.first(), self.book) - self.assertIsNone( - self.user.shelf_set.get(identifier='read').books.first()) + self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first()) readthrough = models.ReadThrough.objects.get(user=self.user) self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.start_date.year, 2020) @@ -179,24 +170,26 @@ class GoodreadsImport(TestCase): self.assertEqual(readthrough.finish_date.month, 10) self.assertEqual(readthrough.finish_date.day, 25) - def test_handle_import_twice(self): - ''' re-importing books ''' - shelf = self.user.shelf_set.filter(identifier='read').first() + """ re-importing books """ + shelf = self.user.shelf_set.filter(identifier="read").first() import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv') - csv_file = open(datafile, 'r') + datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv") + csv_file = open(datafile, "r") for index, entry in enumerate(list(csv.DictReader(csv_file))): entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=index, data=entry, book=self.book) + job_id=import_job.id, index=index, data=entry, book=self.book + ) break - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'public') + self.importer.service, self.user, import_item, False, "public" + ) importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'public') + self.importer.service, self.user, import_item, False, "public" + ) shelf.refresh_from_db() self.assertEqual(shelf.books.first(), self.book) @@ -211,42 +204,44 @@ class GoodreadsImport(TestCase): self.assertEqual(readthrough.finish_date.month, 10) self.assertEqual(readthrough.finish_date.day, 25) - def test_handle_imported_book_review(self): - ''' goodreads review import ''' + """ goodreads review import """ import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv') - csv_file = open(datafile, 'r') + datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv") + csv_file = open(datafile, "r") entry = list(csv.DictReader(csv_file))[2] entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=0, data=entry, book=self.book) + job_id=import_job.id, index=0, data=entry, book=self.book + ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, True, 'unlisted') + self.importer.service, self.user, import_item, True, "unlisted" + ) review = models.Review.objects.get(book=self.book, user=self.user) - self.assertEqual(review.content, 'mixed feelings') + self.assertEqual(review.content, "mixed feelings") self.assertEqual(review.rating, 2) self.assertEqual(review.published_date.year, 2019) self.assertEqual(review.published_date.month, 7) self.assertEqual(review.published_date.day, 8) - self.assertEqual(review.privacy, 'unlisted') - + self.assertEqual(review.privacy, "unlisted") def test_handle_imported_book_reviews_disabled(self): - ''' goodreads review import ''' + """ goodreads review import """ import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv') - csv_file = open(datafile, 'r') + datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv") + csv_file = open(datafile, "r") entry = list(csv.DictReader(csv_file))[2] entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=0, data=entry, book=self.book) + job_id=import_job.id, index=0, data=entry, book=self.book + ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'unlisted') - self.assertFalse(models.Review.objects.filter( - book=self.book, user=self.user - ).exists()) + self.importer.service, self.user, import_item, False, "unlisted" + ) + self.assertFalse( + models.Review.objects.filter(book=self.book, user=self.user).exists() + ) diff --git a/bookwyrm/tests/test_librarything_import.py b/bookwyrm/tests/test_librarything_import.py index 2623a5047..a8e4cfe4f 100644 --- a/bookwyrm/tests/test_librarything_import.py +++ b/bookwyrm/tests/test_librarything_import.py @@ -1,4 +1,4 @@ -''' testing import ''' +""" testing import """ from collections import namedtuple import csv import pathlib @@ -11,114 +11,110 @@ from bookwyrm import models, importer from bookwyrm.librarything_import import LibrarythingImporter from bookwyrm.settings import DOMAIN + class LibrarythingImport(TestCase): - ''' importing from librarything tsv ''' + """ importing from librarything tsv """ + def setUp(self): self.importer = LibrarythingImporter() - ''' use a test tsv ''' - datafile = pathlib.Path(__file__).parent.joinpath( - 'data/librarything.tsv') + """ use a test tsv """ + datafile = pathlib.Path(__file__).parent.joinpath("data/librarything.tsv") # Librarything generates latin encoded exports... - self.csv = open(datafile, 'r', encoding=self.importer.encoding) + self.csv = open(datafile, "r", encoding=self.importer.encoding) self.user = models.User.objects.create_user( - 'mmai', 'mmai@mmai.mmai', 'password', local=True) + "mmai", "mmai@mmai.mmai", "password", local=True + ) models.Connector.objects.create( identifier=DOMAIN, - name='Local', + name="Local", local=True, - connector_file='self_connector', - base_url='https://%s' % DOMAIN, - books_url='https://%s/book' % DOMAIN, - covers_url='https://%s/images/covers' % DOMAIN, - search_url='https://%s/search?q=' % DOMAIN, + connector_file="self_connector", + base_url="https://%s" % DOMAIN, + books_url="https://%s/book" % DOMAIN, + covers_url="https://%s/images/covers" % DOMAIN, + search_url="https://%s/search?q=" % DOMAIN, priority=1, ) - work = models.Work.objects.create(title='Test Work') + work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=work, ) - def test_create_job(self): - ''' creates the import job entry and checks csv ''' - import_job = self.importer.create_job( - self.user, self.csv, False, 'public') + """ creates the import job entry and checks csv """ + import_job = self.importer.create_job(self.user, self.csv, False, "public") self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.include_reviews, False) - self.assertEqual(import_job.privacy, 'public') + self.assertEqual(import_job.privacy, "public") import_items = models.ImportItem.objects.filter(job=import_job).all() self.assertEqual(len(import_items), 3) self.assertEqual(import_items[0].index, 0) - self.assertEqual(import_items[0].data['Book Id'], '5498194') + self.assertEqual(import_items[0].data["Book Id"], "5498194") self.assertEqual(import_items[1].index, 1) - self.assertEqual(import_items[1].data['Book Id'], '5015319') + self.assertEqual(import_items[1].data["Book Id"], "5015319") self.assertEqual(import_items[2].index, 2) - self.assertEqual(import_items[2].data['Book Id'], '5015399') - + self.assertEqual(import_items[2].data["Book Id"], "5015399") def test_create_retry_job(self): - ''' trying again with items that didn't import ''' - import_job = self.importer.create_job( - self.user, self.csv, False, 'unlisted') - import_items = models.ImportItem.objects.filter( - job=import_job - ).all()[:2] + """ trying again with items that didn't import """ + import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") + import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] - retry = self.importer.create_retry_job( - self.user, import_job, import_items) + retry = self.importer.create_retry_job(self.user, import_job, import_items) self.assertNotEqual(import_job, retry) self.assertEqual(retry.user, self.user) self.assertEqual(retry.include_reviews, False) - self.assertEqual(retry.privacy, 'unlisted') + self.assertEqual(retry.privacy, "unlisted") retry_items = models.ImportItem.objects.filter(job=retry).all() self.assertEqual(len(retry_items), 2) self.assertEqual(retry_items[0].index, 0) - self.assertEqual(import_items[0].data['Book Id'], '5498194') + self.assertEqual(import_items[0].data["Book Id"], "5498194") self.assertEqual(retry_items[1].index, 1) - self.assertEqual(retry_items[1].data['Book Id'], '5015319') - + self.assertEqual(retry_items[1].data["Book Id"], "5015319") @responses.activate def test_import_data(self): - ''' resolve entry ''' - import_job = self.importer.create_job( - self.user, self.csv, False, 'unlisted') - book = models.Edition.objects.create(title='Test Book') + """ resolve entry """ + import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") + book = models.Edition.objects.create(title="Test Book") with patch( - 'bookwyrm.models.import_job.ImportItem.get_book_from_isbn' - ) as resolve: + "bookwyrm.models.import_job.ImportItem.get_book_from_isbn" + ) as resolve: resolve.return_value = book - with patch('bookwyrm.importer.handle_imported_book'): + with patch("bookwyrm.importer.handle_imported_book"): importer.import_data(self.importer.service, import_job.id) import_item = models.ImportItem.objects.get(job=import_job, index=0) self.assertEqual(import_item.book.id, book.id) - def test_handle_imported_book(self): - ''' librarything import added a book, this adds related connections ''' - shelf = self.user.shelf_set.filter(identifier='read').first() + """ librarything import added a book, this adds related connections """ + shelf = self.user.shelf_set.filter(identifier="read").first() self.assertIsNone(shelf.books.first()) import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/librarything.tsv') - csv_file = open(datafile, 'r', encoding=self.importer.encoding) - for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))): + datafile = pathlib.Path(__file__).parent.joinpath("data/librarything.tsv") + csv_file = open(datafile, "r", encoding=self.importer.encoding) + for index, entry in enumerate( + list(csv.DictReader(csv_file, delimiter=self.importer.delimiter)) + ): entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=index, data=entry, book=self.book) + job_id=import_job.id, index=index, data=entry, book=self.book + ) break - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'public') + self.importer.service, self.user, import_item, False, "public" + ) shelf.refresh_from_db() self.assertEqual(shelf.books.first(), self.book) @@ -133,31 +129,32 @@ class LibrarythingImport(TestCase): self.assertEqual(readthrough.finish_date.month, 5) self.assertEqual(readthrough.finish_date.day, 8) - def test_handle_imported_book_already_shelved(self): - ''' librarything import added a book, this adds related connections ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - shelf = self.user.shelf_set.filter(identifier='to-read').first() - models.ShelfBook.objects.create( - shelf=shelf, user=self.user, book=self.book) + """ librarything import added a book, this adds related connections """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + shelf = self.user.shelf_set.filter(identifier="to-read").first() + models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/librarything.tsv') - csv_file = open(datafile, 'r', encoding=self.importer.encoding) - for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))): + datafile = pathlib.Path(__file__).parent.joinpath("data/librarything.tsv") + csv_file = open(datafile, "r", encoding=self.importer.encoding) + for index, entry in enumerate( + list(csv.DictReader(csv_file, delimiter=self.importer.delimiter)) + ): entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=index, data=entry, book=self.book) + job_id=import_job.id, index=index, data=entry, book=self.book + ) break - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'public') + self.importer.service, self.user, import_item, False, "public" + ) shelf.refresh_from_db() self.assertEqual(shelf.books.first(), self.book) - self.assertIsNone( - self.user.shelf_set.get(identifier='read').books.first()) + self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first()) readthrough = models.ReadThrough.objects.get(user=self.user) self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.start_date.year, 2007) @@ -167,24 +164,28 @@ class LibrarythingImport(TestCase): self.assertEqual(readthrough.finish_date.month, 5) self.assertEqual(readthrough.finish_date.day, 8) - def test_handle_import_twice(self): - ''' re-importing books ''' - shelf = self.user.shelf_set.filter(identifier='read').first() + """ re-importing books """ + shelf = self.user.shelf_set.filter(identifier="read").first() import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/librarything.tsv') - csv_file = open(datafile, 'r', encoding=self.importer.encoding) - for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))): + datafile = pathlib.Path(__file__).parent.joinpath("data/librarything.tsv") + csv_file = open(datafile, "r", encoding=self.importer.encoding) + for index, entry in enumerate( + list(csv.DictReader(csv_file, delimiter=self.importer.delimiter)) + ): entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=index, data=entry, book=self.book) + job_id=import_job.id, index=index, data=entry, book=self.book + ) break - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'public') + self.importer.service, self.user, import_item, False, "public" + ) importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'public') + self.importer.service, self.user, import_item, False, "public" + ) shelf.refresh_from_db() self.assertEqual(shelf.books.first(), self.book) @@ -199,42 +200,44 @@ class LibrarythingImport(TestCase): self.assertEqual(readthrough.finish_date.month, 5) self.assertEqual(readthrough.finish_date.day, 8) - def test_handle_imported_book_review(self): - ''' librarything review import ''' + """ librarything review import """ import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/librarything.tsv') - csv_file = open(datafile, 'r', encoding=self.importer.encoding) + datafile = pathlib.Path(__file__).parent.joinpath("data/librarything.tsv") + csv_file = open(datafile, "r", encoding=self.importer.encoding) entry = list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))[0] entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=0, data=entry, book=self.book) + job_id=import_job.id, index=0, data=entry, book=self.book + ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, True, 'unlisted') + self.importer.service, self.user, import_item, True, "unlisted" + ) review = models.Review.objects.get(book=self.book, user=self.user) - self.assertEqual(review.content, 'chef d\'oeuvre') + self.assertEqual(review.content, "chef d'oeuvre") self.assertEqual(review.rating, 5) self.assertEqual(review.published_date.year, 2007) self.assertEqual(review.published_date.month, 5) self.assertEqual(review.published_date.day, 8) - self.assertEqual(review.privacy, 'unlisted') - + self.assertEqual(review.privacy, "unlisted") def test_handle_imported_book_reviews_disabled(self): - ''' librarything review import ''' + """ librarything review import """ import_job = models.ImportJob.objects.create(user=self.user) - datafile = pathlib.Path(__file__).parent.joinpath('data/librarything.tsv') - csv_file = open(datafile, 'r', encoding=self.importer.encoding) + datafile = pathlib.Path(__file__).parent.joinpath("data/librarything.tsv") + csv_file = open(datafile, "r", encoding=self.importer.encoding) entry = list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))[2] entry = self.importer.parse_fields(entry) import_item = models.ImportItem.objects.create( - job_id=import_job.id, index=0, data=entry, book=self.book) + job_id=import_job.id, index=0, data=entry, book=self.book + ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): importer.handle_imported_book( - self.importer.service, self.user, import_item, False, 'unlisted') - self.assertFalse(models.Review.objects.filter( - book=self.book, user=self.user - ).exists()) + self.importer.service, self.user, import_item, False, "unlisted" + ) + self.assertFalse( + models.Review.objects.filter(book=self.book, user=self.user).exists() + ) diff --git a/bookwyrm/tests/test_sanitize_html.py b/bookwyrm/tests/test_sanitize_html.py index 58d94311c..2b3d0378d 100644 --- a/bookwyrm/tests/test_sanitize_html.py +++ b/bookwyrm/tests/test_sanitize_html.py @@ -1,28 +1,30 @@ -''' make sure only valid html gets to the app ''' +""" make sure only valid html gets to the app """ from django.test import TestCase from bookwyrm.sanitize_html import InputHtmlParser + class Sanitizer(TestCase): - ''' sanitizer tests ''' + """ sanitizer tests """ + def test_no_html(self): - ''' just text ''' - input_text = 'no html ' + """ just text """ + input_text = "no html " parser = InputHtmlParser() parser.feed(input_text) output = parser.get_output() self.assertEqual(input_text, output) def test_valid_html(self): - ''' leave the html untouched ''' - input_text = 'yes html' + """ leave the html untouched """ + input_text = "yes html" parser = InputHtmlParser() parser.feed(input_text) output = parser.get_output() self.assertEqual(input_text, output) def test_valid_html_attrs(self): - ''' and don't remove attributes ''' + """ and don't remove attributes """ input_text = 'yes html' parser = InputHtmlParser() parser.feed(input_text) @@ -30,23 +32,23 @@ class Sanitizer(TestCase): self.assertEqual(input_text, output) def test_invalid_html(self): - ''' remove all html when the html is malformed ''' - input_text = 'yes html' + """ remove all html when the html is malformed """ + input_text = "yes html" parser = InputHtmlParser() parser.feed(input_text) output = parser.get_output() - self.assertEqual('yes html', output) + self.assertEqual("yes html", output) - input_text = 'yes html ' + input_text = "yes html " parser = InputHtmlParser() parser.feed(input_text) output = parser.get_output() - self.assertEqual('yes html ', output) + self.assertEqual("yes html ", output) def test_disallowed_html(self): - ''' remove disallowed html but keep allowed html ''' - input_text = '
yes html
' + """ remove disallowed html but keep allowed html """ + input_text = "
yes html
" parser = InputHtmlParser() parser.feed(input_text) output = parser.get_output() - self.assertEqual(' yes html', output) + self.assertEqual(" yes html", output) diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py index f6de11e1e..d9cc411c0 100644 --- a/bookwyrm/tests/test_signing.py +++ b/bookwyrm/tests/test_signing.py @@ -1,4 +1,4 @@ -''' getting and verifying signatures ''' +""" getting and verifying signatures """ import time from collections import namedtuple from urllib.parse import urlsplit @@ -18,141 +18,125 @@ from bookwyrm.activitypub import Follow from bookwyrm.settings import DOMAIN from bookwyrm.signatures import create_key_pair, make_signature, make_digest + def get_follow_activity(follower, followee): - ''' generates a test activity ''' + """ generates a test activity """ return Follow( - id='https://test.com/user/follow/id', + id="https://test.com/user/follow/id", actor=follower.remote_id, object=followee.remote_id, ).serialize() -KeyPair = namedtuple('KeyPair', ('private_key', 'public_key')) -Sender = namedtuple('Sender', ('remote_id', 'key_pair')) + +KeyPair = namedtuple("KeyPair", ("private_key", "public_key")) +Sender = namedtuple("Sender", ("remote_id", "key_pair")) + class Signature(TestCase): - ''' signature test ''' + """ signature test """ + def setUp(self): - ''' create users and test data ''' + """ create users and test data """ self.mouse = models.User.objects.create_user( - 'mouse@%s' % DOMAIN, 'mouse@example.com', '', - local=True, localname='mouse') + "mouse@%s" % DOMAIN, "mouse@example.com", "", local=True, localname="mouse" + ) self.rat = models.User.objects.create_user( - 'rat@%s' % DOMAIN, 'rat@example.com', '', - local=True, localname='rat') + "rat@%s" % DOMAIN, "rat@example.com", "", local=True, localname="rat" + ) self.cat = models.User.objects.create_user( - 'cat@%s' % DOMAIN, 'cat@example.com', '', - local=True, localname='cat') + "cat@%s" % DOMAIN, "cat@example.com", "", local=True, localname="cat" + ) private_key, public_key = create_key_pair() self.fake_remote = Sender( - 'http://localhost/user/remote', - KeyPair(private_key, public_key) + "http://localhost/user/remote", KeyPair(private_key, public_key) ) models.SiteSettings.objects.create() def send(self, signature, now, data, digest): - ''' test request ''' + """ test request """ c = Client() return c.post( urlsplit(self.rat.inbox).path, data=data, - content_type='application/json', + content_type="application/json", **{ - 'HTTP_DATE': now, - 'HTTP_SIGNATURE': signature, - 'HTTP_DIGEST': digest, - 'HTTP_CONTENT_TYPE': 'application/activity+json; charset=utf-8', - 'HTTP_HOST': DOMAIN, + "HTTP_DATE": now, + "HTTP_SIGNATURE": signature, + "HTTP_DIGEST": digest, + "HTTP_CONTENT_TYPE": "application/activity+json; charset=utf-8", + "HTTP_HOST": DOMAIN, } ) - def send_test_request(#pylint: disable=too-many-arguments - self, - sender, - signer=None, - send_data=None, - digest=None, - date=None): - ''' sends a follow request to the "rat" user ''' + def send_test_request( # pylint: disable=too-many-arguments + self, sender, signer=None, send_data=None, digest=None, date=None + ): + """ sends a follow request to the "rat" user """ now = date or http_date() data = json.dumps(get_follow_activity(sender, self.rat)) digest = digest or make_digest(data) - signature = make_signature( - signer or sender, self.rat.inbox, now, digest) - with patch('bookwyrm.views.inbox.activity_task.delay'): - with patch('bookwyrm.models.user.set_remote_server.delay'): + signature = make_signature(signer or sender, self.rat.inbox, now, digest) + with patch("bookwyrm.views.inbox.activity_task.delay"): + with patch("bookwyrm.models.user.set_remote_server.delay"): return self.send(signature, now, send_data or data, digest) def test_correct_signature(self): - ''' this one should just work ''' + """ this one should just work """ response = self.send_test_request(sender=self.mouse) self.assertEqual(response.status_code, 200) def test_wrong_signature(self): - ''' Messages must be signed by the right actor. - (cat cannot sign messages on behalf of mouse) ''' + """Messages must be signed by the right actor. + (cat cannot sign messages on behalf of mouse)""" response = self.send_test_request(sender=self.mouse, signer=self.cat) self.assertEqual(response.status_code, 401) @responses.activate def test_remote_signer(self): - ''' signtures for remote users ''' - datafile = pathlib.Path(__file__).parent.joinpath('data/ap_user.json') + """ signtures for remote users """ + datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json") data = json.loads(datafile.read_bytes()) - data['id'] = self.fake_remote.remote_id - data['publicKey']['publicKeyPem'] = self.fake_remote.key_pair.public_key - del data['icon'] # Avoid having to return an avatar. + data["id"] = self.fake_remote.remote_id + data["publicKey"]["publicKeyPem"] = self.fake_remote.key_pair.public_key + del data["icon"] # Avoid having to return an avatar. + responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200) + responses.add( + responses.GET, "https://localhost/.well-known/nodeinfo", status=404 + ) responses.add( responses.GET, - self.fake_remote.remote_id, - json=data, - status=200) - responses.add( - responses.GET, - 'https://localhost/.well-known/nodeinfo', - status=404) - responses.add( - responses.GET, - 'https://example.com/user/mouse/outbox?page=true', - json={'orderedItems': []}, - status=200 + "https://example.com/user/mouse/outbox?page=true", + json={"orderedItems": []}, + status=200, ) - with patch('bookwyrm.models.user.get_remote_reviews.delay'): + with patch("bookwyrm.models.user.get_remote_reviews.delay"): response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200) @responses.activate def test_key_needs_refresh(self): - ''' an out of date key should be updated and the new key work ''' - datafile = pathlib.Path(__file__).parent.joinpath('data/ap_user.json') + """ an out of date key should be updated and the new key work """ + datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json") data = json.loads(datafile.read_bytes()) - data['id'] = self.fake_remote.remote_id - data['publicKey']['publicKeyPem'] = self.fake_remote.key_pair.public_key - del data['icon'] # Avoid having to return an avatar. + data["id"] = self.fake_remote.remote_id + data["publicKey"]["publicKeyPem"] = self.fake_remote.key_pair.public_key + del data["icon"] # Avoid having to return an avatar. + responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200) responses.add( - responses.GET, - self.fake_remote.remote_id, - json=data, - status=200) - responses.add( - responses.GET, - 'https://localhost/.well-known/nodeinfo', - status=404) + responses.GET, "https://localhost/.well-known/nodeinfo", status=404 + ) # Second and subsequent fetches get a different key: key_pair = KeyPair(*create_key_pair()) new_sender = Sender(self.fake_remote.remote_id, key_pair) - data['publicKey']['publicKeyPem'] = key_pair.public_key - responses.add( - responses.GET, - self.fake_remote.remote_id, - json=data, - status=200) + data["publicKey"]["publicKeyPem"] = key_pair.public_key + responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200) - with patch('bookwyrm.models.user.get_remote_reviews.delay'): + with patch("bookwyrm.models.user.get_remote_reviews.delay"): # Key correct: response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200) @@ -169,43 +153,42 @@ class Signature(TestCase): response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 401) - @responses.activate def test_nonexistent_signer(self): - ''' fail when unable to look up signer ''' + """ fail when unable to look up signer """ responses.add( responses.GET, self.fake_remote.remote_id, - json={'error': 'not found'}, - status=404) + json={"error": "not found"}, + status=404, + ) response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 401) @pytest.mark.integration def test_changed_data(self): - '''Message data must match the digest header.''' - with patch('bookwyrm.activitypub.resolve_remote_id'): + """Message data must match the digest header.""" + with patch("bookwyrm.activitypub.resolve_remote_id"): response = self.send_test_request( - self.mouse, - send_data=get_follow_activity(self.mouse, self.cat)) + self.mouse, send_data=get_follow_activity(self.mouse, self.cat) + ) self.assertEqual(response.status_code, 401) @pytest.mark.integration def test_invalid_digest(self): - ''' signature digest must be valid ''' - with patch('bookwyrm.activitypub.resolve_remote_id'): + """ signature digest must be valid """ + with patch("bookwyrm.activitypub.resolve_remote_id"): response = self.send_test_request( - self.mouse, - digest='SHA-256=AAAAAAAAAAAAAAAAAA') + self.mouse, digest="SHA-256=AAAAAAAAAAAAAAAAAA" + ) self.assertEqual(response.status_code, 401) @pytest.mark.integration def test_old_message(self): - '''Old messages should be rejected to prevent replay attacks.''' - with patch('bookwyrm.activitypub.resolve_remote_id'): + """Old messages should be rejected to prevent replay attacks.""" + with patch("bookwyrm.activitypub.resolve_remote_id"): response = self.send_test_request( - self.mouse, - date=http_date(time.time() - 301) + self.mouse, date=http_date(time.time() - 301) ) self.assertEqual(response.status_code, 401) diff --git a/bookwyrm/tests/test_templatetags.py b/bookwyrm/tests/test_templatetags.py index 45c993449..6bdbd04c0 100644 --- a/bookwyrm/tests/test_templatetags.py +++ b/bookwyrm/tests/test_templatetags.py @@ -1,4 +1,4 @@ -''' style fixes and lookups for templates ''' +""" style fixes and lookups for templates """ import re from unittest.mock import patch @@ -11,82 +11,85 @@ from bookwyrm.templatetags import bookwyrm_tags class TemplateTags(TestCase): - ''' lotta different things here ''' - def setUp(self): - ''' create some filler objects ''' - self.user = models.User.objects.create_user( - 'mouse@example.com', 'mouse@mouse.mouse', 'mouseword', - local=True, localname='mouse') - with patch('bookwyrm.models.user.set_remote_server.delay'): - self.remote_user = models.User.objects.create_user( - 'rat', 'rat@rat.rat', 'ratword', - remote_id='http://example.com/rat', local=False) - self.book = models.Edition.objects.create(title='Test Book') + """ lotta different things here """ + def setUp(self): + """ create some filler objects """ + self.user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.mouse", + "mouseword", + local=True, + localname="mouse", + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.rat", + "ratword", + remote_id="http://example.com/rat", + local=False, + ) + self.book = models.Edition.objects.create(title="Test Book") def test_dict_key(self): - ''' just getting a value out of a dict ''' - test_dict = {'a': 1, 'b': 3} - self.assertEqual( - bookwyrm_tags.dict_key(test_dict, 'a'), 1) - self.assertEqual( - bookwyrm_tags.dict_key(test_dict, 'c'), 0) - + """ just getting a value out of a dict """ + test_dict = {"a": 1, "b": 3} + self.assertEqual(bookwyrm_tags.dict_key(test_dict, "a"), 1) + self.assertEqual(bookwyrm_tags.dict_key(test_dict, "c"), 0) def test_get_user_rating(self): - ''' get a user's most recent rating of a book ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.Review.objects.create( - user=self.user, book=self.book, rating=3) - self.assertEqual( - bookwyrm_tags.get_user_rating(self.book, self.user), 3) - + """ get a user's most recent rating of a book """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.Review.objects.create(user=self.user, book=self.book, rating=3) + self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 3) def test_get_user_rating_doesnt_exist(self): - ''' there is no rating available ''' - self.assertEqual( - bookwyrm_tags.get_user_rating(self.book, self.user), 0) - + """ there is no rating available """ + self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 0) def test_get_user_identifer_local(self): - ''' fall back to the simplest uid available ''' + """ fall back to the simplest uid available """ self.assertNotEqual(self.user.username, self.user.localname) - self.assertEqual( - bookwyrm_tags.get_user_identifier(self.user), 'mouse') + self.assertEqual(bookwyrm_tags.get_user_identifier(self.user), "mouse") def test_get_user_identifer_remote(self): - ''' for a remote user, should be their full username ''' + """ for a remote user, should be their full username """ self.assertEqual( - bookwyrm_tags.get_user_identifier(self.remote_user), - 'rat@example.com') + bookwyrm_tags.get_user_identifier(self.remote_user), "rat@example.com" + ) def test_get_notification_count(self): - ''' just countin' ''' + """ just countin' """ self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0) - models.Notification.objects.create( - user=self.user, notification_type='FAVORITE') - models.Notification.objects.create( - user=self.user, notification_type='MENTION') + models.Notification.objects.create(user=self.user, notification_type="FAVORITE") + models.Notification.objects.create(user=self.user, notification_type="MENTION") models.Notification.objects.create( - user=self.remote_user, notification_type='FOLLOW') + user=self.remote_user, notification_type="FOLLOW" + ) self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 2) - def test_get_replies(self): - ''' direct replies to a status ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ direct replies to a status """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): parent = models.Review.objects.create( - user=self.user, book=self.book, content='hi') + user=self.user, book=self.book, content="hi" + ) first_child = models.Status.objects.create( - reply_parent=parent, user=self.user, content='hi') + reply_parent=parent, user=self.user, content="hi" + ) second_child = models.Status.objects.create( - reply_parent=parent, user=self.user, content='hi') + reply_parent=parent, user=self.user, content="hi" + ) third_child = models.Status.objects.create( - reply_parent=parent, user=self.user, - deleted=True, deleted_date=timezone.now()) + reply_parent=parent, + user=self.user, + deleted=True, + deleted_date=timezone.now(), + ) replies = bookwyrm_tags.get_replies(parent) self.assertEqual(len(replies), 2) @@ -94,181 +97,162 @@ class TemplateTags(TestCase): self.assertTrue(second_child in replies) self.assertFalse(third_child in replies) - def test_get_parent(self): - ''' get the reply parent of a status ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ get the reply parent of a status """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): parent = models.Review.objects.create( - user=self.user, book=self.book, content='hi') + user=self.user, book=self.book, content="hi" + ) child = models.Status.objects.create( - reply_parent=parent, user=self.user, content='hi') + reply_parent=parent, user=self.user, content="hi" + ) result = bookwyrm_tags.get_parent(child) self.assertEqual(result, parent) self.assertIsInstance(result, models.Review) - def test_get_user_liked(self): - ''' did a user like a status ''' - status = models.Review.objects.create( - user=self.remote_user, book=self.book) + """ did a user like a status """ + status = models.Review.objects.create(user=self.remote_user, book=self.book) self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status)) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.Favorite.objects.create( - user=self.user, - status=status - ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.Favorite.objects.create(user=self.user, status=status) self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status)) - def test_get_user_boosted(self): - ''' did a user boost a status ''' - status = models.Review.objects.create( - user=self.remote_user, book=self.book) + """ did a user boost a status """ + status = models.Review.objects.create(user=self.remote_user, book=self.book) self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status)) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.Boost.objects.create( - user=self.user, - boosted_status=status - ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.Boost.objects.create(user=self.user, boosted_status=status) self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status)) - def test_follow_request_exists(self): - ''' does a user want to follow ''' + """ does a user want to follow """ self.assertFalse( - bookwyrm_tags.follow_request_exists(self.user, self.remote_user)) + bookwyrm_tags.follow_request_exists(self.user, self.remote_user) + ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.UserFollowRequest.objects.create( - user_subject=self.user, - user_object=self.remote_user) + user_subject=self.user, user_object=self.remote_user + ) self.assertFalse( - bookwyrm_tags.follow_request_exists(self.user, self.remote_user)) + bookwyrm_tags.follow_request_exists(self.user, self.remote_user) + ) self.assertTrue( - bookwyrm_tags.follow_request_exists(self.remote_user, self.user)) - + bookwyrm_tags.follow_request_exists(self.remote_user, self.user) + ) def test_get_boosted(self): - ''' load a boosted status ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Review.objects.create( - user=self.remote_user, book=self.book) - boost = models.Boost.objects.create( - user=self.user, - boosted_status=status - ) + """ load a boosted status """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Review.objects.create(user=self.remote_user, book=self.book) + boost = models.Boost.objects.create(user=self.user, boosted_status=status) boosted = bookwyrm_tags.get_boosted(boost) self.assertIsInstance(boosted, models.Review) self.assertEqual(boosted, status) - def test_get_book_description(self): - ''' grab it from the edition or the parent ''' - work = models.Work.objects.create(title='Test Work') + """ grab it from the edition or the parent """ + work = models.Work.objects.create(title="Test Work") self.book.parent_work = work self.book.save() self.assertIsNone(bookwyrm_tags.get_book_description(self.book)) - work.description = 'hi' + work.description = "hi" work.save() - self.assertEqual(bookwyrm_tags.get_book_description(self.book), 'hi') + self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hi") - self.book.description = 'hello' + self.book.description = "hello" self.book.save() - self.assertEqual(bookwyrm_tags.get_book_description(self.book), 'hello') - + self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello") def test_get_uuid(self): - ''' uuid functionality ''' - uuid = bookwyrm_tags.get_uuid('hi') - self.assertTrue(re.match(r'hi[A-Za-z0-9\-]', uuid)) - + """ uuid functionality """ + uuid = bookwyrm_tags.get_uuid("hi") + self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid)) def test_time_since(self): - ''' ultraconcise timestamps ''' - self.assertEqual(bookwyrm_tags.time_since('bleh'), '') + """ ultraconcise timestamps """ + self.assertEqual(bookwyrm_tags.time_since("bleh"), "") now = timezone.now() - self.assertEqual(bookwyrm_tags.time_since(now), '0s') + self.assertEqual(bookwyrm_tags.time_since(now), "0s") seconds_ago = now - relativedelta(seconds=4) - self.assertEqual(bookwyrm_tags.time_since(seconds_ago), '4s') + self.assertEqual(bookwyrm_tags.time_since(seconds_ago), "4s") minutes_ago = now - relativedelta(minutes=8) - self.assertEqual(bookwyrm_tags.time_since(minutes_ago), '8m') + self.assertEqual(bookwyrm_tags.time_since(minutes_ago), "8m") hours_ago = now - relativedelta(hours=9) - self.assertEqual(bookwyrm_tags.time_since(hours_ago), '9h') + self.assertEqual(bookwyrm_tags.time_since(hours_ago), "9h") days_ago = now - relativedelta(days=3) - self.assertEqual(bookwyrm_tags.time_since(days_ago), '3d') + self.assertEqual(bookwyrm_tags.time_since(days_ago), "3d") # I am not going to figure out how to mock dates tonight. months_ago = now - relativedelta(months=5) - self.assertTrue(re.match( - r'[A-Z][a-z]{2} \d?\d', - bookwyrm_tags.time_since(months_ago) - )) + self.assertTrue( + re.match(r"[A-Z][a-z]{2} \d?\d", bookwyrm_tags.time_since(months_ago)) + ) years_ago = now - relativedelta(years=10) - self.assertTrue(re.match( - r'[A-Z][a-z]{2} \d?\d \d{4}', - bookwyrm_tags.time_since(years_ago) - )) - + self.assertTrue( + re.match(r"[A-Z][a-z]{2} \d?\d \d{4}", bookwyrm_tags.time_since(years_ago)) + ) def test_get_markdown(self): - ''' mardown format data ''' - result = bookwyrm_tags.get_markdown('_hi_') - self.assertEqual(result, '

hi

') - - result = bookwyrm_tags.get_markdown('_hi_') - self.assertEqual(result, '

hi

') + """ mardown format data """ + result = bookwyrm_tags.get_markdown("_hi_") + self.assertEqual(result, "

hi

") + result = bookwyrm_tags.get_markdown("_hi_") + self.assertEqual(result, "

hi

") def test_get_mentions(self): - ''' list of people mentioned ''' - status = models.Status.objects.create( - content='hi', user=self.remote_user) + """ list of people mentioned """ + status = models.Status.objects.create(content="hi", user=self.remote_user) result = bookwyrm_tags.get_mentions(status, self.user) - self.assertEqual(result, '@rat@example.com ') - + self.assertEqual(result, "@rat@example.com ") def test_get_status_preview_name(self): - ''' status context string ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create(content='hi', user=self.user) + """ status context string """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(content="hi", user=self.user) result = bookwyrm_tags.get_status_preview_name(status) - self.assertEqual(result, 'status') + self.assertEqual(result, "status") status = models.Review.objects.create( - content='hi', user=self.user, book=self.book) + content="hi", user=self.user, book=self.book + ) result = bookwyrm_tags.get_status_preview_name(status) - self.assertEqual(result, 'review of Test Book') + self.assertEqual(result, "review of Test Book") status = models.Comment.objects.create( - content='hi', user=self.user, book=self.book) + content="hi", user=self.user, book=self.book + ) result = bookwyrm_tags.get_status_preview_name(status) - self.assertEqual(result, 'comment on Test Book') + self.assertEqual(result, "comment on Test Book") status = models.Quotation.objects.create( - content='hi', user=self.user, book=self.book) + content="hi", user=self.user, book=self.book + ) result = bookwyrm_tags.get_status_preview_name(status) - self.assertEqual(result, 'quotation from Test Book') - + self.assertEqual(result, "quotation from Test Book") def test_related_status(self): - ''' gets the subclass model for a notification status ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create(content='hi', user=self.user) + """ gets the subclass model for a notification status """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(content="hi", user=self.user) notification = models.Notification.objects.create( - user=self.user, notification_type='MENTION', - related_status=status) + user=self.user, notification_type="MENTION", related_status=status + ) result = bookwyrm_tags.related_status(notification) self.assertIsInstance(result, models.Status) diff --git a/bookwyrm/tests/views/test_authentication.py b/bookwyrm/tests/views/test_authentication.py index dc52719c9..f6d318615 100644 --- a/bookwyrm/tests/views/test_authentication.py +++ b/bookwyrm/tests/views/test_authentication.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.contrib.auth.models import AnonymousUser @@ -14,21 +14,26 @@ from bookwyrm.settings import DOMAIN # pylint: disable=too-many-public-methods class AuthenticationViews(TestCase): - ''' login and password management ''' + """ login and password management """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'password', - local=True, localname='mouse') + "mouse@local.com", + "mouse@mouse.com", + "password", + local=True, + localname="mouse", + ) self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False self.settings = models.SiteSettings.objects.create(id=1) def test_login_get(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ login = views.Login.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.anonymous_user result = login(request) @@ -38,135 +43,117 @@ class AuthenticationViews(TestCase): request.user = self.local_user result = login(request) - self.assertEqual(result.url, '/') + self.assertEqual(result.url, "/") self.assertEqual(result.status_code, 302) - def test_register(self): - ''' create a user ''' + """ create a user """ view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( - 'register/', + "register/", { - 'localname': 'nutria-user.user_nutria', - 'password': 'mouseword', - 'email': 'aa@bb.cccc' - }) - with patch('bookwyrm.views.authentication.login'): + "localname": "nutria-user.user_nutria", + "password": "mouseword", + "email": "aa@bb.cccc", + }, + ) + with patch("bookwyrm.views.authentication.login"): response = view(request) self.assertEqual(models.User.objects.count(), 2) self.assertEqual(response.status_code, 302) nutria = models.User.objects.last() - self.assertEqual(nutria.username, 'nutria-user.user_nutria@%s' % DOMAIN) - self.assertEqual(nutria.localname, 'nutria-user.user_nutria') + self.assertEqual(nutria.username, "nutria-user.user_nutria@%s" % DOMAIN) + self.assertEqual(nutria.localname, "nutria-user.user_nutria") self.assertEqual(nutria.local, True) def test_register_trailing_space(self): - ''' django handles this so weirdly ''' + """ django handles this so weirdly """ view = views.Register.as_view() request = self.factory.post( - 'register/', - { - 'localname': 'nutria ', - 'password': 'mouseword', - 'email': 'aa@bb.ccc' - }) - with patch('bookwyrm.views.authentication.login'): + "register/", + {"localname": "nutria ", "password": "mouseword", "email": "aa@bb.ccc"}, + ) + with patch("bookwyrm.views.authentication.login"): response = view(request) self.assertEqual(models.User.objects.count(), 2) self.assertEqual(response.status_code, 302) nutria = models.User.objects.last() - self.assertEqual(nutria.username, 'nutria@%s' % DOMAIN) - self.assertEqual(nutria.localname, 'nutria') + self.assertEqual(nutria.username, "nutria@%s" % DOMAIN) + self.assertEqual(nutria.localname, "nutria") self.assertEqual(nutria.local, True) def test_register_invalid_email(self): - ''' gotta have an email ''' + """ gotta have an email """ view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( - 'register/', - { - 'localname': 'nutria', - 'password': 'mouseword', - 'email': 'aa' - }) + "register/", {"localname": "nutria", "password": "mouseword", "email": "aa"} + ) response = view(request) self.assertEqual(models.User.objects.count(), 1) response.render() def test_register_invalid_username(self): - ''' gotta have an email ''' + """ gotta have an email """ view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( - 'register/', - { - 'localname': 'nut@ria', - 'password': 'mouseword', - 'email': 'aa@bb.ccc' - }) + "register/", + {"localname": "nut@ria", "password": "mouseword", "email": "aa@bb.ccc"}, + ) response = view(request) self.assertEqual(models.User.objects.count(), 1) response.render() request = self.factory.post( - 'register/', - { - 'localname': 'nutr ia', - 'password': 'mouseword', - 'email': 'aa@bb.ccc' - }) + "register/", + {"localname": "nutr ia", "password": "mouseword", "email": "aa@bb.ccc"}, + ) response = view(request) self.assertEqual(models.User.objects.count(), 1) response.render() request = self.factory.post( - 'register/', - { - 'localname': 'nut@ria', - 'password': 'mouseword', - 'email': 'aa@bb.ccc' - }) + "register/", + {"localname": "nut@ria", "password": "mouseword", "email": "aa@bb.ccc"}, + ) response = view(request) self.assertEqual(models.User.objects.count(), 1) response.render() - def test_register_closed_instance(self): - ''' you can't just register ''' + """ you can't just register """ view = views.Register.as_view() self.settings.allow_registration = False self.settings.save() request = self.factory.post( - 'register/', - { - 'localname': 'nutria ', - 'password': 'mouseword', - 'email': 'aa@bb.ccc' - }) + "register/", + {"localname": "nutria ", "password": "mouseword", "email": "aa@bb.ccc"}, + ) with self.assertRaises(PermissionDenied): view(request) def test_register_invite(self): - ''' you can't just register ''' + """ you can't just register """ view = views.Register.as_view() self.settings.allow_registration = False self.settings.save() models.SiteInvite.objects.create( - code='testcode', user=self.local_user, use_limit=1) + code="testcode", user=self.local_user, use_limit=1 + ) self.assertEqual(models.SiteInvite.objects.get().times_used, 0) request = self.factory.post( - 'register/', + "register/", { - 'localname': 'nutria', - 'password': 'mouseword', - 'email': 'aa@bb.ccc', - 'invite_code': 'testcode' - }) - with patch('bookwyrm.views.authentication.login'): + "localname": "nutria", + "password": "mouseword", + "email": "aa@bb.ccc", + "invite_code": "testcode", + }, + ) + with patch("bookwyrm.views.authentication.login"): response = view(request) self.assertEqual(models.User.objects.count(), 2) self.assertEqual(response.status_code, 302) @@ -174,26 +161,28 @@ class AuthenticationViews(TestCase): # invite already used to max capacity request = self.factory.post( - 'register/', + "register/", { - 'localname': 'nutria2', - 'password': 'mouseword', - 'email': 'aa@bb.ccc', - 'invite_code': 'testcode' - }) + "localname": "nutria2", + "password": "mouseword", + "email": "aa@bb.ccc", + "invite_code": "testcode", + }, + ) with self.assertRaises(PermissionDenied): response = view(request) self.assertEqual(models.User.objects.count(), 2) # bad invite code request = self.factory.post( - 'register/', + "register/", { - 'localname': 'nutria3', - 'password': 'mouseword', - 'email': 'aa@bb.ccc', - 'invite_code': 'dkfkdjgdfkjgkdfj' - }) + "localname": "nutria3", + "password": "mouseword", + "email": "aa@bb.ccc", + "invite_code": "dkfkdjgdfkjgkdfj", + }, + ) with self.assertRaises(Http404): response = view(request) self.assertEqual(models.User.objects.count(), 2) diff --git a/bookwyrm/tests/views/test_author.py b/bookwyrm/tests/views/test_author.py index 3c1a68bb2..bb047b7c1 100644 --- a/bookwyrm/tests/views/test_author.py +++ b/bookwyrm/tests/views/test_author.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType @@ -12,37 +12,41 @@ from bookwyrm.activitypub import ActivitypubResponse class AuthorViews(TestCase): - ''' author views''' + """ author views""" + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - self.group = Group.objects.create(name='editor') + self.group = Group.objects.create(name="editor") self.group.permissions.add( Permission.objects.create( - name='edit_book', - codename='edit_book', - content_type=ContentType.objects.get_for_model(models.User)).id + name="edit_book", + codename="edit_book", + content_type=ContentType.objects.get_for_model(models.User), + ).id ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=self.work, ) models.SiteSettings.objects.create() - def test_author_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Author.as_view() - author = models.Author.objects.create(name='Jessica') - request = self.factory.get('') - with patch('bookwyrm.views.author.is_api_request') as is_api: + author = models.Author.objects.create(name="Jessica") + request = self.factory.get("") + with patch("bookwyrm.views.author.is_api_request") as is_api: is_api.return_value = False result = view(request, author.id) self.assertIsInstance(result, TemplateResponse) @@ -50,19 +54,18 @@ class AuthorViews(TestCase): self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200) - request = self.factory.get('') - with patch('bookwyrm.views.author.is_api_request') as is_api: + request = self.factory.get("") + with patch("bookwyrm.views.author.is_api_request") as is_api: is_api.return_value = True result = view(request, author.id) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_edit_author_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.EditAuthor.as_view() - author = models.Author.objects.create(name='Test Author') - request = self.factory.get('') + author = models.Author.objects.create(name="Test Author") + request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True @@ -72,52 +75,51 @@ class AuthorViews(TestCase): self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200) - def test_edit_author(self): - ''' edit an author ''' + """ edit an author """ view = views.EditAuthor.as_view() - author = models.Author.objects.create(name='Test Author') + author = models.Author.objects.create(name="Test Author") self.local_user.groups.add(self.group) form = forms.AuthorForm(instance=author) - form.data['name'] = 'New Name' - form.data['last_edited_by'] = self.local_user.id - request = self.factory.post('', form.data) + form.data["name"] = "New Name" + 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'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): view(request, author.id) author.refresh_from_db() - self.assertEqual(author.name, 'New Name') + self.assertEqual(author.name, "New Name") self.assertEqual(author.last_edited_by, self.local_user) def test_edit_author_non_editor(self): - ''' edit an author with invalid post data''' + """ edit an author with invalid post data""" view = views.EditAuthor.as_view() - author = models.Author.objects.create(name='Test Author') + author = models.Author.objects.create(name="Test Author") form = forms.AuthorForm(instance=author) - form.data['name'] = 'New Name' - form.data['last_edited_by'] = self.local_user.id - request = self.factory.post('', form.data) + form.data["name"] = "New Name" + form.data["last_edited_by"] = self.local_user.id + request = self.factory.post("", form.data) request.user = self.local_user with self.assertRaises(PermissionDenied): view(request, author.id) author.refresh_from_db() - self.assertEqual(author.name, 'Test Author') + self.assertEqual(author.name, "Test Author") def test_edit_author_invalid_form(self): - ''' edit an author with invalid post data''' + """ edit an author with invalid post data""" view = views.EditAuthor.as_view() - author = models.Author.objects.create(name='Test Author') + author = models.Author.objects.create(name="Test Author") self.local_user.groups.add(self.group) form = forms.AuthorForm(instance=author) - form.data['name'] = '' - form.data['last_edited_by'] = self.local_user.id - request = self.factory.post('', form.data) + form.data["name"] = "" + form.data["last_edited_by"] = self.local_user.id + request = self.factory.post("", form.data) request.user = self.local_user resp = view(request, author.id) author.refresh_from_db() - self.assertEqual(author.name, 'Test Author') + self.assertEqual(author.name, "Test Author") resp.render() self.assertEqual(resp.status_code, 200) diff --git a/bookwyrm/tests/views/test_block.py b/bookwyrm/tests/views/test_block.py index 315dc2247..60920e38b 100644 --- a/bookwyrm/tests/views/test_block.py +++ b/bookwyrm/tests/views/test_block.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase @@ -8,28 +8,34 @@ from bookwyrm import models, views class BlockViews(TestCase): - ''' view user and edit profile ''' + """ view user and edit profile """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ 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') - with patch('bookwyrm.models.user.set_remote_server.delay'): + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'ratword', + "rat", + "rat@rat.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) models.SiteSettings.objects.create() - def test_block_get(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Block.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) @@ -37,19 +43,19 @@ class BlockViews(TestCase): self.assertEqual(result.status_code, 200) def test_block_post(self): - ''' create a "block" database entry from an activity ''' + """ create a "block" database entry from an activity """ view = views.Block.as_view() self.local_user.followers.add(self.remote_user) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user) + user_subject=self.local_user, user_object=self.remote_user + ) self.assertTrue(models.UserFollows.objects.exists()) self.assertTrue(models.UserFollowRequest.objects.exists()) - request = self.factory.post('') + request = self.factory.post("") 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.remote_user.id) block = models.UserBlocks.objects.get() self.assertEqual(block.user_subject, self.local_user) @@ -59,12 +65,12 @@ class BlockViews(TestCase): self.assertFalse(models.UserFollowRequest.objects.exists()) def test_unblock(self): - ''' undo a block ''' + """ undo a block """ self.local_user.blocks.add(self.remote_user) - request = self.factory.post('') + request = self.factory.post("") request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.block.unblock(request, self.remote_user.id) self.assertFalse(models.UserBlocks.objects.exists()) diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index b3360200d..eb8c89b98 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType @@ -11,55 +11,58 @@ from bookwyrm.activitypub import ActivitypubResponse class BookViews(TestCase): - ''' books books books ''' + """ books books books """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - self.group = Group.objects.create(name='editor') + self.group = Group.objects.create(name="editor") self.group.permissions.add( Permission.objects.create( - name='edit_book', - codename='edit_book', - content_type=ContentType.objects.get_for_model(models.User)).id + name="edit_book", + codename="edit_book", + content_type=ContentType.objects.get_for_model(models.User), + ).id ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=self.work, ) models.SiteSettings.objects.create() - def test_book_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Book.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.views.books.is_api_request') as is_api: + with patch("bookwyrm.views.books.is_api_request") as is_api: is_api.return_value = False result = view(request, self.book.id) self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - request = self.factory.get('') - with patch('bookwyrm.views.books.is_api_request') as is_api: + request = self.factory.get("") + with patch("bookwyrm.views.books.is_api_request") as is_api: is_api.return_value = True result = view(request, self.book.id) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_edit_book_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.EditBook.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True result = view(request, self.book.id) @@ -67,66 +70,57 @@ class BookViews(TestCase): result.render() self.assertEqual(result.status_code, 200) - def test_edit_book(self): - ''' lets a user edit a book ''' + """ 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 - request = self.factory.post('', form.data) + 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'): + 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") def test_switch_edition(self): - ''' updates user's relationships to a book ''' - work = models.Work.objects.create(title='test work') - edition1 = models.Edition.objects.create( - title='first ed', parent_work=work) - edition2 = models.Edition.objects.create( - title='second ed', parent_work=work) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - shelf = models.Shelf.objects.create( - name='Test Shelf', user=self.local_user) + """ updates user's relationships to a book """ + work = models.Work.objects.create(title="test work") + edition1 = models.Edition.objects.create(title="first ed", parent_work=work) + edition2 = models.Edition.objects.create(title="second ed", parent_work=work) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user) models.ShelfBook.objects.create( book=edition1, user=self.local_user, shelf=shelf, ) - models.ReadThrough.objects.create( - user=self.local_user, book=edition1) + models.ReadThrough.objects.create(user=self.local_user, book=edition1) self.assertEqual(models.ShelfBook.objects.get().book, edition1) self.assertEqual(models.ReadThrough.objects.get().book, edition1) - request = self.factory.post('', { - 'edition': edition2.id - }) + request = self.factory.post("", {"edition": edition2.id}) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.switch_edition(request) self.assertEqual(models.ShelfBook.objects.get().book, edition2) self.assertEqual(models.ReadThrough.objects.get().book, edition2) - def test_editions_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Editions.as_view() - request = self.factory.get('') - with patch('bookwyrm.views.books.is_api_request') as is_api: + request = self.factory.get("") + with patch("bookwyrm.views.books.is_api_request") as is_api: is_api.return_value = False result = view(request, self.work.id) self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - request = self.factory.get('') - with patch('bookwyrm.views.books.is_api_request') as is_api: + request = self.factory.get("") + with patch("bookwyrm.views.books.is_api_request") as is_api: is_api.return_value = True result = view(request, self.work.id) self.assertIsInstance(result, ActivitypubResponse) diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py index 70cf41f6e..d11d7415f 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/test_federation.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -8,20 +8,24 @@ from bookwyrm import views class FederationViews(TestCase): - ''' every response to a get request, html or json ''' + """ every response to a get request, html or json """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ 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') + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) models.SiteSettings.objects.create() - def test_federation_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Federation.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True result = view(request) diff --git a/bookwyrm/tests/views/test_feed.py b/bookwyrm/tests/views/test_feed.py index 93af9944b..c54be0061 100644 --- a/bookwyrm/tests/views/test_feed.py +++ b/bookwyrm/tests/views/test_feed.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase @@ -10,95 +10,93 @@ from bookwyrm.activitypub import ActivitypubResponse class FeedMessageViews(TestCase): - ''' dms ''' + """ dms """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ 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') + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) self.book = models.Edition.objects.create( - parent_work=models.Work.objects.create(title='hi'), - title='Example Edition', - remote_id='https://example.com/book/1', + parent_work=models.Work.objects.create(title="hi"), + title="Example Edition", + remote_id="https://example.com/book/1", ) models.SiteSettings.objects.create() - def test_feed(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Feed.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user - result = view(request, 'local') + result = view(request, "local") self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - def test_status_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Status.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create( - content='hi', user=self.local_user) - request = self.factory.get('') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(content="hi", user=self.local_user) + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.views.feed.is_api_request') as is_api: + with patch("bookwyrm.views.feed.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'mouse', status.id) + result = view(request, "mouse", status.id) self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - with patch('bookwyrm.views.feed.is_api_request') as is_api: + with patch("bookwyrm.views.feed.is_api_request") as is_api: is_api.return_value = True - result = view(request, 'mouse', status.id) + result = view(request, "mouse", status.id) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_replies_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Replies.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create( - content='hi', user=self.local_user) - request = self.factory.get('') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(content="hi", user=self.local_user) + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.views.feed.is_api_request') as is_api: + with patch("bookwyrm.views.feed.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'mouse', status.id) + result = view(request, "mouse", status.id) self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - with patch('bookwyrm.views.feed.is_api_request') as is_api: + with patch("bookwyrm.views.feed.is_api_request") as is_api: is_api.return_value = True - result = view(request, 'mouse', status.id) + result = view(request, "mouse", status.id) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_direct_messages_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.DirectMessage.as_view() - request = self.factory.get('') + 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_get_suggested_book(self): - ''' gets books the ~*~ algorithm ~*~ thinks you want to post about ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ gets books the ~*~ algorithm ~*~ thinks you want to post about """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( book=self.book, user=self.local_user, - shelf=self.local_user.shelf_set.get(identifier='reading') + shelf=self.local_user.shelf_set.get(identifier="reading"), ) suggestions = views.feed.get_suggested_books(self.local_user) - self.assertEqual(suggestions[0]['name'], 'Currently Reading') - self.assertEqual(suggestions[0]['books'][0], self.book) + self.assertEqual(suggestions[0]["name"], "Currently Reading") + self.assertEqual(suggestions[0]["books"][0], self.book) diff --git a/bookwyrm/tests/views/test_follow.py b/bookwyrm/tests/views/test_follow.py index 62543d2de..67ac0f0b8 100644 --- a/bookwyrm/tests/views/test_follow.py +++ b/bookwyrm/tests/views/test_follow.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType @@ -9,148 +9,147 @@ from bookwyrm import models, views class BookViews(TestCase): - ''' books books books ''' + """ books books books """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - with patch('bookwyrm.models.user.set_remote_server'): + with patch("bookwyrm.models.user.set_remote_server"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@email.com', 'ratword', + "rat", + "rat@email.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) - self.group = Group.objects.create(name='editor') + self.group = Group.objects.create(name="editor") self.group.permissions.add( Permission.objects.create( - name='edit_book', - codename='edit_book', - content_type=ContentType.objects.get_for_model(models.User)).id + name="edit_book", + codename="edit_book", + content_type=ContentType.objects.get_for_model(models.User), + ).id ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=self.work, ) def test_handle_follow_remote(self): - ''' send a follow request ''' - request = self.factory.post('', {'user': self.remote_user.username}) + """ send a follow request """ + request = self.factory.post("", {"user": self.remote_user.username}) request.user = self.local_user self.assertEqual(models.UserFollowRequest.objects.count(), 0) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.follow(request) rel = models.UserFollowRequest.objects.get() self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_object, self.remote_user) - self.assertEqual(rel.status, 'follow_request') - + self.assertEqual(rel.status, "follow_request") def test_handle_follow_local_manually_approves(self): - ''' send a follow request ''' + """ send a follow request """ rat = models.User.objects.create_user( - 'rat@local.com', 'rat@rat.com', 'ratword', - local=True, localname='rat', - remote_id='https://example.com/users/rat', + "rat@local.com", + "rat@rat.com", + "ratword", + local=True, + localname="rat", + remote_id="https://example.com/users/rat", manually_approves_followers=True, ) - request = self.factory.post('', {'user': rat}) + request = self.factory.post("", {"user": rat}) request.user = self.local_user self.assertEqual(models.UserFollowRequest.objects.count(), 0) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.follow(request) rel = models.UserFollowRequest.objects.get() self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_object, rat) - self.assertEqual(rel.status, 'follow_request') - + self.assertEqual(rel.status, "follow_request") def test_handle_follow_local(self): - ''' send a follow request ''' + """ send a follow request """ rat = models.User.objects.create_user( - 'rat@local.com', 'rat@rat.com', 'ratword', - local=True, localname='rat', - remote_id='https://example.com/users/rat', + "rat@local.com", + "rat@rat.com", + "ratword", + local=True, + localname="rat", + remote_id="https://example.com/users/rat", ) - request = self.factory.post('', {'user': rat}) + request = self.factory.post("", {"user": rat}) request.user = self.local_user self.assertEqual(models.UserFollowRequest.objects.count(), 0) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.follow(request) rel = models.UserFollows.objects.get() self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_object, rat) - self.assertEqual(rel.status, 'follows') - + self.assertEqual(rel.status, "follows") def test_handle_unfollow(self): - ''' send an unfollow ''' - request = self.factory.post('', {'user': self.remote_user.username}) + """ send an unfollow """ + request = self.factory.post("", {"user": self.remote_user.username}) request.user = self.local_user self.remote_user.followers.add(self.local_user) self.assertEqual(self.remote_user.followers.count(), 1) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ - as mock: + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: views.unfollow(request) self.assertEqual(mock.call_count, 1) self.assertEqual(self.remote_user.followers.count(), 0) - def test_handle_accept(self): - ''' accept a follow request ''' + """ accept a follow request """ self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) - request = self.factory.post('', {'user': self.remote_user.username}) + request = self.factory.post("", {"user": self.remote_user.username}) request.user = self.local_user rel = models.UserFollowRequest.objects.create( - user_subject=self.remote_user, - user_object=self.local_user + user_subject=self.remote_user, user_object=self.local_user ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.accept_follow_request(request) # request should be deleted - self.assertEqual( - models.UserFollowRequest.objects.filter(id=rel.id).count(), 0 - ) + self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0) # follow relationship should exist self.assertEqual(self.local_user.followers.first(), self.remote_user) - def test_handle_reject(self): - ''' reject a follow request ''' + """ reject a follow request """ self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) - request = self.factory.post('', {'user': self.remote_user.username}) + request = self.factory.post("", {"user": self.remote_user.username}) request.user = self.local_user rel = models.UserFollowRequest.objects.create( - user_subject=self.remote_user, - user_object=self.local_user + user_subject=self.remote_user, user_object=self.local_user ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.delete_follow_request(request) # request should be deleted - self.assertEqual( - models.UserFollowRequest.objects.filter(id=rel.id).count(), 0 - ) + self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0) # follow relationship should not exist - self.assertEqual( - models.UserFollows.objects.filter(id=rel.id).count(), 0 - ) + self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0) diff --git a/bookwyrm/tests/views/test_goal.py b/bookwyrm/tests/views/test_goal.py index 0d534112a..990bb5c2b 100644 --- a/bookwyrm/tests/views/test_goal.py +++ b/bookwyrm/tests/views/test_goal.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.utils import timezone @@ -11,60 +11,65 @@ from bookwyrm import models, views class GoalViews(TestCase): - ''' viewing and creating statuses ''' + """ viewing and creating statuses """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) self.rat = models.User.objects.create_user( - 'rat@local.com', 'rat@rat.com', 'ratword', - local=True, localname='rat', - remote_id='https://example.com/users/rat', + "rat@local.com", + "rat@rat.com", + "ratword", + local=True, + localname="rat", + remote_id="https://example.com/users/rat", ) self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', + title="Example Edition", + remote_id="https://example.com/book/1", ) self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False models.SiteSettings.objects.create() - def test_goal_page_no_goal(self): - ''' view a reading goal page for another's unset goal ''' + """ view a reading goal page for another's unset goal """ view = views.Goal.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.rat result = view(request, self.local_user.localname, 2020) self.assertEqual(result.status_code, 404) def test_goal_page_no_goal_self(self): - ''' view a reading goal page for your own unset goal ''' + """ view a reading goal page for your own unset goal """ view = views.Goal.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user result = view(request, self.local_user.localname, 2020) result.render() self.assertIsInstance(result, TemplateResponse) - def test_goal_page_anonymous(self): - ''' can't view it without login ''' + """ can't view it without login """ view = views.Goal.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.anonymous_user result = view(request, self.local_user.localname, 2020) self.assertEqual(result.status_code, 302) def test_goal_page_public(self): - ''' view a user's public goal ''' + """ view a user's public goal """ models.ReadThrough.objects.create( finish_date=timezone.now(), user=self.local_user, @@ -75,9 +80,10 @@ class GoalViews(TestCase): user=self.local_user, year=timezone.now().year, goal=128937123, - privacy='public') + privacy="public", + ) view = views.Goal.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.rat result = view(request, self.local_user.localname, timezone.now().year) @@ -85,40 +91,40 @@ class GoalViews(TestCase): self.assertIsInstance(result, TemplateResponse) def test_goal_page_private(self): - ''' view a user's private goal ''' + """ view a user's private goal """ models.AnnualGoal.objects.create( - user=self.local_user, - year=2020, - goal=15, - privacy='followers') + user=self.local_user, year=2020, goal=15, privacy="followers" + ) view = views.Goal.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.rat result = view(request, self.local_user.localname, 2020) self.assertEqual(result.status_code, 404) - def test_create_goal(self): - ''' create a new goal ''' + """ create a new goal """ view = views.Goal.as_view() - request = self.factory.post('', { - 'user': self.local_user.id, - 'goal': 10, - 'year': 2020, - 'privacy': 'unlisted', - 'post-status': True - }) + request = self.factory.post( + "", + { + "user": self.local_user.id, + "goal": 10, + "year": 2020, + "privacy": "unlisted", + "post-status": True, + }, + ) 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.local_user.localname, 2020) goal = models.AnnualGoal.objects.get() self.assertEqual(goal.user, self.local_user) self.assertEqual(goal.goal, 10) self.assertEqual(goal.year, 2020) - self.assertEqual(goal.privacy, 'unlisted') + self.assertEqual(goal.privacy, "unlisted") status = models.GeneratedNote.objects.get() self.assertEqual(status.user, self.local_user) - self.assertEqual(status.privacy, 'unlisted') + self.assertEqual(status.privacy, "unlisted") diff --git a/bookwyrm/tests/views/test_helpers.py b/bookwyrm/tests/views/test_helpers.py index eff083072..bb4cf69c4 100644 --- a/bookwyrm/tests/views/test_helpers.py +++ b/bookwyrm/tests/views/test_helpers.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ import json from unittest.mock import patch import pathlib @@ -9,127 +9,129 @@ import responses from bookwyrm import models, views from bookwyrm.settings import USER_AGENT + class ViewsHelpers(TestCase): - ''' viewing and creating statuses ''' + """ viewing and creating statuses """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Test Book', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Test Book", + remote_id="https://example.com/book/1", + parent_work=self.work, ) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'ratword', + "rat", + "rat@rat.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_user.json' - ) + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") self.userdata = json.loads(datafile.read_bytes()) - del self.userdata['icon'] - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + del self.userdata["icon"] + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): self.shelf = models.Shelf.objects.create( - name='Test Shelf', - identifier='test-shelf', - user=self.local_user + name="Test Shelf", identifier="test-shelf", user=self.local_user ) - def test_get_edition(self): - ''' given an edition or a work, returns an edition ''' - self.assertEqual( - views.helpers.get_edition(self.book.id), self.book) - self.assertEqual( - views.helpers.get_edition(self.work.id), self.book) + """ given an edition or a work, returns an edition """ + self.assertEqual(views.helpers.get_edition(self.book.id), self.book) + self.assertEqual(views.helpers.get_edition(self.work.id), self.book) def test_get_user_from_username(self): - ''' works for either localname or username ''' + """ works for either localname or username """ self.assertEqual( - views.helpers.get_user_from_username( - self.local_user, 'mouse'), self.local_user) + views.helpers.get_user_from_username(self.local_user, "mouse"), + self.local_user, + ) self.assertEqual( - views.helpers.get_user_from_username( - self.local_user, 'mouse@local.com'), self.local_user) + views.helpers.get_user_from_username(self.local_user, "mouse@local.com"), + self.local_user, + ) with self.assertRaises(models.User.DoesNotExist): - views.helpers.get_user_from_username( - self.local_user, 'mojfse@example.com') - + views.helpers.get_user_from_username(self.local_user, "mojfse@example.com") def test_is_api_request(self): - ''' should it return html or json ''' - request = self.factory.get('/path') - request.headers = {'Accept': 'application/json'} + """ should it return html or json """ + request = self.factory.get("/path") + request.headers = {"Accept": "application/json"} self.assertTrue(views.helpers.is_api_request(request)) - request = self.factory.get('/path.json') - request.headers = {'Accept': 'Praise'} + request = self.factory.get("/path.json") + request.headers = {"Accept": "Praise"} self.assertTrue(views.helpers.is_api_request(request)) - request = self.factory.get('/path') - request.headers = {'Accept': 'Praise'} + request = self.factory.get("/path") + request.headers = {"Accept": "Praise"} self.assertFalse(views.helpers.is_api_request(request)) - def test_get_activity_feed(self): - ''' loads statuses ''' + """ loads statuses """ rat = models.User.objects.create_user( - 'rat', 'rat@rat.rat', 'password', local=True) + "rat", "rat@rat.rat", "password", local=True + ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): public_status = models.Comment.objects.create( - content='public status', book=self.book, user=self.local_user) + content="public status", book=self.book, user=self.local_user + ) direct_status = models.Status.objects.create( - content='direct', user=self.local_user, privacy='direct') + content="direct", user=self.local_user, privacy="direct" + ) - rat_public = models.Status.objects.create( - content='blah blah', user=rat) + rat_public = models.Status.objects.create(content="blah blah", user=rat) rat_unlisted = models.Status.objects.create( - content='blah blah', user=rat, privacy='unlisted') + content="blah blah", user=rat, privacy="unlisted" + ) remote_status = models.Status.objects.create( - content='blah blah', user=self.remote_user) + content="blah blah", user=self.remote_user + ) followers_status = models.Status.objects.create( - content='blah', user=rat, privacy='followers') + content="blah", user=rat, privacy="followers" + ) rat_mention = models.Status.objects.create( - content='blah blah blah', user=rat, privacy='followers') + content="blah blah blah", user=rat, privacy="followers" + ) rat_mention.mention_users.set([self.local_user]) statuses = views.helpers.get_activity_feed( self.local_user, - privacy=['public', 'unlisted', 'followers'], + privacy=["public", "unlisted", "followers"], following_only=True, - queryset=models.Comment.objects + queryset=models.Comment.objects, ) self.assertEqual(len(statuses), 1) self.assertEqual(statuses[0], public_status) statuses = views.helpers.get_activity_feed( - self.local_user, - privacy=['public', 'followers'], - local_only=True + self.local_user, privacy=["public", "followers"], local_only=True ) self.assertEqual(len(statuses), 2) self.assertEqual(statuses[1], public_status) self.assertEqual(statuses[0], rat_public) - statuses = views.helpers.get_activity_feed( - self.local_user, privacy=['direct']) + statuses = views.helpers.get_activity_feed(self.local_user, privacy=["direct"]) self.assertEqual(len(statuses), 1) self.assertEqual(statuses[0], direct_status) statuses = views.helpers.get_activity_feed( self.local_user, - privacy=['public', 'followers'], + privacy=["public", "followers"], ) self.assertEqual(len(statuses), 3) self.assertEqual(statuses[2], public_status) @@ -138,8 +140,8 @@ class ViewsHelpers(TestCase): statuses = views.helpers.get_activity_feed( self.local_user, - privacy=['public', 'unlisted', 'followers'], - following_only=True + privacy=["public", "unlisted", "followers"], + following_only=True, ) self.assertEqual(len(statuses), 2) self.assertEqual(statuses[1], public_status) @@ -148,8 +150,8 @@ class ViewsHelpers(TestCase): rat.followers.add(self.local_user) statuses = views.helpers.get_activity_feed( self.local_user, - privacy=['public', 'unlisted', 'followers'], - following_only=True + privacy=["public", "unlisted", "followers"], + following_only=True, ) self.assertEqual(len(statuses), 5) self.assertEqual(statuses[4], public_status) @@ -158,187 +160,187 @@ class ViewsHelpers(TestCase): self.assertEqual(statuses[1], followers_status) self.assertEqual(statuses[0], rat_mention) - def test_get_activity_feed_blocks(self): - ''' feed generation with blocked users ''' + """ feed generation with blocked users """ rat = models.User.objects.create_user( - 'rat', 'rat@rat.rat', 'password', local=True) + "rat", "rat@rat.rat", "password", local=True + ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): public_status = models.Comment.objects.create( - content='public status', book=self.book, user=self.local_user) - rat_public = models.Status.objects.create( - content='blah blah', user=rat) + content="public status", book=self.book, user=self.local_user + ) + rat_public = models.Status.objects.create(content="blah blah", user=rat) statuses = views.helpers.get_activity_feed( - self.local_user, privacy=['public']) + self.local_user, privacy=["public"] + ) self.assertEqual(len(statuses), 2) # block relationship rat.blocks.add(self.local_user) - statuses = views.helpers.get_activity_feed( - self.local_user, privacy=['public']) + statuses = views.helpers.get_activity_feed(self.local_user, privacy=["public"]) self.assertEqual(len(statuses), 1) self.assertEqual(statuses[0], public_status) - statuses = views.helpers.get_activity_feed( - rat, privacy=['public']) + statuses = views.helpers.get_activity_feed(rat, privacy=["public"]) self.assertEqual(len(statuses), 1) self.assertEqual(statuses[0], rat_public) - - def test_is_bookwyrm_request(self): - ''' checks if a request came from a bookwyrm instance ''' - request = self.factory.get('', {'q': 'Test Book'}) + """ checks if a request came from a bookwyrm instance """ + request = self.factory.get("", {"q": "Test Book"}) self.assertFalse(views.helpers.is_bookwyrm_request(request)) request = self.factory.get( - '', {'q': 'Test Book'}, - HTTP_USER_AGENT=\ - "http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)" + "", + {"q": "Test Book"}, + HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", ) self.assertFalse(views.helpers.is_bookwyrm_request(request)) - request = self.factory.get( - '', {'q': 'Test Book'}, HTTP_USER_AGENT=USER_AGENT) + request = self.factory.get("", {"q": "Test Book"}, HTTP_USER_AGENT=USER_AGENT) self.assertTrue(views.helpers.is_bookwyrm_request(request)) - def test_existing_user(self): - ''' simple database lookup by username ''' - result = views.helpers.handle_remote_webfinger('@mouse@local.com') + """ simple database lookup by username """ + result = views.helpers.handle_remote_webfinger("@mouse@local.com") self.assertEqual(result, self.local_user) - result = views.helpers.handle_remote_webfinger('mouse@local.com') + result = views.helpers.handle_remote_webfinger("mouse@local.com") self.assertEqual(result, self.local_user) - @responses.activate def test_load_user(self): - ''' find a remote user using webfinger ''' - username = 'mouse@example.com' + """ find a remote user using webfinger """ + username = "mouse@example.com" wellknown = { "subject": "acct:mouse@example.com", - "links": [{ - "rel": "self", - "type": "application/activity+json", - "href": "https://example.com/user/mouse" - }] + "links": [ + { + "rel": "self", + "type": "application/activity+json", + "href": "https://example.com/user/mouse", + } + ], } responses.add( responses.GET, - 'https://example.com/.well-known/webfinger?resource=acct:%s' \ - % username, + "https://example.com/.well-known/webfinger?resource=acct:%s" % username, json=wellknown, - status=200) + status=200, + ) responses.add( responses.GET, - 'https://example.com/user/mouse', + "https://example.com/user/mouse", json=self.userdata, - status=200) - with patch('bookwyrm.models.user.set_remote_server.delay'): - result = views.helpers.handle_remote_webfinger('@mouse@example.com') + status=200, + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + result = views.helpers.handle_remote_webfinger("@mouse@example.com") self.assertIsInstance(result, models.User) - self.assertEqual(result.username, 'mouse@example.com') - + self.assertEqual(result.username, "mouse@example.com") def test_handle_reading_status_to_read(self): - ''' posts shelve activities ''' - shelf = self.local_user.shelf_set.get(identifier='to-read') - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ posts shelve activities """ + shelf = self.local_user.shelf_set.get(identifier="to-read") + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( - self.local_user, shelf, self.book, 'public') + self.local_user, shelf, self.book, "public" + ) status = models.GeneratedNote.objects.get() self.assertEqual(status.user, self.local_user) self.assertEqual(status.mention_books.first(), self.book) - self.assertEqual(status.content, 'wants to read') + self.assertEqual(status.content, "wants to read") def test_handle_reading_status_reading(self): - ''' posts shelve activities ''' - shelf = self.local_user.shelf_set.get(identifier='reading') - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ posts shelve activities """ + shelf = self.local_user.shelf_set.get(identifier="reading") + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( - self.local_user, shelf, self.book, 'public') + self.local_user, shelf, self.book, "public" + ) status = models.GeneratedNote.objects.get() self.assertEqual(status.user, self.local_user) self.assertEqual(status.mention_books.first(), self.book) - self.assertEqual(status.content, 'started reading') + self.assertEqual(status.content, "started reading") def test_handle_reading_status_read(self): - ''' posts shelve activities ''' - shelf = self.local_user.shelf_set.get(identifier='read') - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ posts shelve activities """ + shelf = self.local_user.shelf_set.get(identifier="read") + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( - self.local_user, shelf, self.book, 'public') + self.local_user, shelf, self.book, "public" + ) status = models.GeneratedNote.objects.get() self.assertEqual(status.user, self.local_user) self.assertEqual(status.mention_books.first(), self.book) - self.assertEqual(status.content, 'finished reading') + self.assertEqual(status.content, "finished reading") def test_handle_reading_status_other(self): - ''' posts shelve activities ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ posts shelve activities """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( - self.local_user, self.shelf, self.book, 'public') + self.local_user, self.shelf, self.book, "public" + ) self.assertFalse(models.GeneratedNote.objects.exists()) def test_object_visible_to_user(self): - ''' does a user have permission to view an object ''' + """ does a user have permission to view an object """ obj = models.Status.objects.create( - content='hi', user=self.remote_user, privacy='public') - self.assertTrue( - views.helpers.object_visible_to_user(self.local_user, obj)) + content="hi", user=self.remote_user, privacy="public" + ) + self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) obj = models.Shelf.objects.create( - name='test', user=self.remote_user, privacy='unlisted') - self.assertTrue( - views.helpers.object_visible_to_user(self.local_user, obj)) + name="test", user=self.remote_user, privacy="unlisted" + ) + self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) obj = models.Status.objects.create( - content='hi', user=self.remote_user, privacy='followers') - self.assertFalse( - views.helpers.object_visible_to_user(self.local_user, obj)) + content="hi", user=self.remote_user, privacy="followers" + ) + self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) obj = models.Status.objects.create( - content='hi', user=self.remote_user, privacy='direct') - self.assertFalse( - views.helpers.object_visible_to_user(self.local_user, obj)) + content="hi", user=self.remote_user, privacy="direct" + ) + self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) obj = models.Status.objects.create( - content='hi', user=self.remote_user, privacy='direct') + content="hi", user=self.remote_user, privacy="direct" + ) obj.mention_users.add(self.local_user) - self.assertTrue( - views.helpers.object_visible_to_user(self.local_user, obj)) + self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) def test_object_visible_to_user_follower(self): - ''' what you can see if you follow a user ''' + """ what you can see if you follow a user """ self.remote_user.followers.add(self.local_user) obj = models.Status.objects.create( - content='hi', user=self.remote_user, privacy='followers') - self.assertTrue( - views.helpers.object_visible_to_user(self.local_user, obj)) + content="hi", user=self.remote_user, privacy="followers" + ) + self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) obj = models.Status.objects.create( - content='hi', user=self.remote_user, privacy='direct') - self.assertFalse( - views.helpers.object_visible_to_user(self.local_user, obj)) + content="hi", user=self.remote_user, privacy="direct" + ) + self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) obj = models.Status.objects.create( - content='hi', user=self.remote_user, privacy='direct') + content="hi", user=self.remote_user, privacy="direct" + ) obj.mention_users.add(self.local_user) - self.assertTrue( - views.helpers.object_visible_to_user(self.local_user, obj)) + self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) def test_object_visible_to_user_blocked(self): - ''' you can't see it if they block you ''' + """ you can't see it if they block you """ self.remote_user.blocks.add(self.local_user) obj = models.Status.objects.create( - content='hi', user=self.remote_user, privacy='public') - self.assertFalse( - views.helpers.object_visible_to_user(self.local_user, obj)) + content="hi", user=self.remote_user, privacy="public" + ) + self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) obj = models.Shelf.objects.create( - name='test', user=self.remote_user, privacy='unlisted') - self.assertFalse( - views.helpers.object_visible_to_user(self.local_user, obj)) + name="test", user=self.remote_user, privacy="unlisted" + ) + self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) diff --git a/bookwyrm/tests/views/test_import.py b/bookwyrm/tests/views/test_import.py index ba8f2457b..b98b2516a 100644 --- a/bookwyrm/tests/views/test_import.py +++ b/bookwyrm/tests/views/test_import.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase @@ -9,34 +9,37 @@ from bookwyrm import views class ImportViews(TestCase): - ''' goodreads import views ''' + """ goodreads import views """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ 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') + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) models.SiteSettings.objects.create() - def test_import_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Import.as_view() - request = self.factory.get('') + 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_import_status(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.ImportStatus.as_view() import_job = models.ImportJob.objects.create(user=self.local_user) - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.tasks.app.AsyncResult') as async_result: + with patch("bookwyrm.tasks.app.AsyncResult") as async_result: async_result.return_value = [] result = view(request, import_job.id) self.assertIsInstance(result, TemplateResponse) diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index 4202979b3..cab508928 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -1,4 +1,4 @@ -''' tests incoming activities''' +""" tests incoming activities""" from datetime import datetime import json import pathlib @@ -11,101 +11,100 @@ import responses from bookwyrm import models, views -#pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods class Inbox(TestCase): - ''' readthrough tests ''' + """ readthrough tests """ + def setUp(self): - ''' basic user and book data ''' + """ basic user and book data """ self.client = Client() self.local_user = models.User.objects.create_user( - 'mouse@example.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse') - self.local_user.remote_id = 'https://example.com/user/mouse' + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" self.local_user.save(broadcast=False) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'ratword', + "rat", + "rat@rat.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): self.status = models.Status.objects.create( user=self.local_user, - content='Test status', - remote_id='https://example.com/status/1', + content="Test status", + remote_id="https://example.com/status/1", ) self.create_json = { - 'id': 'hi', - 'type': 'Create', - 'actor': 'hi', - "to": [ - "https://www.w3.org/ns/activitystreams#public" - ], - "cc": [ - "https://example.com/user/mouse/followers" - ], - 'object': {} + "id": "hi", + "type": "Create", + "actor": "hi", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": {}, } models.SiteSettings.objects.create() - def test_inbox_invalid_get(self): - ''' shouldn't try to handle if the user is not found ''' - result = self.client.get( - '/inbox', content_type="application/json" - ) + """ shouldn't try to handle if the user is not found """ + result = self.client.get("/inbox", content_type="application/json") self.assertIsInstance(result, HttpResponseNotAllowed) def test_inbox_invalid_user(self): - ''' shouldn't try to handle if the user is not found ''' + """ shouldn't try to handle if the user is not found """ result = self.client.post( - '/user/bleh/inbox', + "/user/bleh/inbox", '{"type": "Test", "object": "exists"}', - content_type="application/json" + content_type="application/json", ) self.assertIsInstance(result, HttpResponseNotFound) def test_inbox_invalid_bad_signature(self): - ''' bad request for invalid signature ''' - with patch('bookwyrm.views.inbox.has_valid_signature') as mock_valid: + """ bad request for invalid signature """ + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: mock_valid.return_value = False result = self.client.post( - '/user/mouse/inbox', + "/user/mouse/inbox", '{"type": "Announce", "object": "exists"}', - content_type="application/json" + content_type="application/json", ) self.assertEqual(result.status_code, 401) def test_inbox_invalid_bad_signature_delete(self): - ''' invalid signature for Delete is okay though ''' - with patch('bookwyrm.views.inbox.has_valid_signature') as mock_valid: + """ invalid signature for Delete is okay though """ + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: mock_valid.return_value = False result = self.client.post( - '/user/mouse/inbox', + "/user/mouse/inbox", '{"type": "Delete", "object": "exists"}', - content_type="application/json" + content_type="application/json", ) self.assertEqual(result.status_code, 200) def test_inbox_unknown_type(self): - ''' never heard of that activity type, don't have a handler for it ''' - with patch('bookwyrm.views.inbox.has_valid_signature') as mock_valid: + """ never heard of that activity type, don't have a handler for it """ + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: result = self.client.post( - '/inbox', + "/inbox", '{"type": "Fish", "object": "exists"}', - content_type="application/json" + content_type="application/json", ) mock_valid.return_value = True self.assertIsInstance(result, HttpResponseNotFound) - def test_inbox_success(self): - ''' a known type, for which we start a task ''' + """ a known type, for which we start a task """ activity = self.create_json - activity['object'] = { + activity["object"] = { "id": "https://example.com/list/22", "type": "BookList", "totalItems": 1, @@ -113,47 +112,41 @@ class Inbox(TestCase): "last": "https://example.com/list/22?page=1", "name": "Test List", "owner": "https://example.com/user/mouse", - "to": [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "cc": [ - "https://example.com/user/mouse/followers" - ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], "summary": "summary text", "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams" + "@context": "https://www.w3.org/ns/activitystreams", } - with patch('bookwyrm.views.inbox.has_valid_signature') as mock_valid: + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: mock_valid.return_value = True - with patch('bookwyrm.views.inbox.activity_task.delay'): + with patch("bookwyrm.views.inbox.activity_task.delay"): result = self.client.post( - '/inbox', - json.dumps(activity), - content_type="application/json" + "/inbox", json.dumps(activity), content_type="application/json" ) self.assertEqual(result.status_code, 200) - def test_handle_create_status(self): - ''' the "it justs works" mode ''' + """ the "it justs works" mode """ self.assertEqual(models.Status.objects.count(), 1) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_quotation.json') + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_quotation.json") status_data = json.loads(datafile.read_bytes()) models.Edition.objects.create( - title='Test Book', remote_id='https://example.com/book/1') + title="Test Book", remote_id="https://example.com/book/1" + ) activity = self.create_json - activity['object'] = status_data + activity["object"] = status_data views.inbox.activity_task(activity) status = models.Quotation.objects.get() self.assertEqual( - status.remote_id, 'https://example.com/user/mouse/quotation/13') - self.assertEqual(status.quote, 'quote body') - self.assertEqual(status.content, 'commentary') + status.remote_id, "https://example.com/user/mouse/quotation/13" + ) + self.assertEqual(status.quote, "quote body") + self.assertEqual(status.content, "commentary") self.assertEqual(status.user, self.local_user) self.assertEqual(models.Status.objects.count(), 2) @@ -161,56 +154,50 @@ class Inbox(TestCase): views.inbox.activity_task(activity) self.assertEqual(models.Status.objects.count(), 2) - def test_handle_create_status_remote_note_with_mention(self): - ''' should only create it under the right circumstances ''' + """ should only create it under the right circumstances """ self.assertEqual(models.Status.objects.count(), 1) self.assertFalse( - models.Notification.objects.filter(user=self.local_user).exists()) + models.Notification.objects.filter(user=self.local_user).exists() + ) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_note.json') + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_note.json") status_data = json.loads(datafile.read_bytes()) activity = self.create_json - activity['object'] = status_data + activity["object"] = status_data views.inbox.activity_task(activity) status = models.Status.objects.last() - self.assertEqual(status.content, 'test content in note') + self.assertEqual(status.content, "test content in note") self.assertEqual(status.mention_users.first(), self.local_user) self.assertTrue( - models.Notification.objects.filter(user=self.local_user).exists()) - self.assertEqual( - models.Notification.objects.get().notification_type, 'MENTION') + models.Notification.objects.filter(user=self.local_user).exists() + ) + self.assertEqual(models.Notification.objects.get().notification_type, "MENTION") def test_handle_create_status_remote_note_with_reply(self): - ''' should only create it under the right circumstances ''' + """ should only create it under the right circumstances """ self.assertEqual(models.Status.objects.count(), 1) - self.assertFalse( - models.Notification.objects.filter(user=self.local_user)) + self.assertFalse(models.Notification.objects.filter(user=self.local_user)) - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_note.json') + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_note.json") status_data = json.loads(datafile.read_bytes()) - del status_data['tag'] - status_data['inReplyTo'] = self.status.remote_id + del status_data["tag"] + status_data["inReplyTo"] = self.status.remote_id activity = self.create_json - activity['object'] = status_data + activity["object"] = status_data views.inbox.activity_task(activity) status = models.Status.objects.last() - self.assertEqual(status.content, 'test content in note') + self.assertEqual(status.content, "test content in note") self.assertEqual(status.reply_parent, self.status) - self.assertTrue( - models.Notification.objects.filter(user=self.local_user)) - self.assertEqual( - models.Notification.objects.get().notification_type, 'REPLY') - + self.assertTrue(models.Notification.objects.filter(user=self.local_user)) + self.assertEqual(models.Notification.objects.get().notification_type, "REPLY") def test_handle_create_list(self): - ''' a new list ''' + """ a new list """ activity = self.create_json - activity['object'] = { + activity["object"] = { "id": "https://example.com/list/22", "type": "BookList", "totalItems": 1, @@ -218,44 +205,38 @@ class Inbox(TestCase): "last": "https://example.com/list/22?page=1", "name": "Test List", "owner": "https://example.com/user/mouse", - "to": [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "cc": [ - "https://example.com/user/mouse/followers" - ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], "summary": "summary text", "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams" + "@context": "https://www.w3.org/ns/activitystreams", } views.inbox.activity_task(activity) book_list = models.List.objects.get() - self.assertEqual(book_list.name, 'Test List') - self.assertEqual(book_list.curation, 'curated') - self.assertEqual(book_list.description, 'summary text') - self.assertEqual(book_list.remote_id, 'https://example.com/list/22') - + self.assertEqual(book_list.name, "Test List") + self.assertEqual(book_list.curation, "curated") + self.assertEqual(book_list.description, "summary text") + self.assertEqual(book_list.remote_id, "https://example.com/list/22") def test_handle_follow_x(self): - ''' remote user wants to follow local user ''' + """ remote user wants to follow local user """ activity = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/users/rat/follows/123", "type": "Follow", "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" + "object": "https://example.com/user/mouse", } self.assertFalse(models.UserFollowRequest.objects.exists()) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ - as mock: + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: views.inbox.activity_task(activity) self.assertEqual(mock.call_count, 1) # notification created notification = models.Notification.objects.get() self.assertEqual(notification.user, self.local_user) - self.assertEqual(notification.notification_type, 'FOLLOW') + self.assertEqual(notification.notification_type, "FOLLOW") # the request should have been deleted self.assertFalse(models.UserFollowRequest.objects.exists()) @@ -264,27 +245,26 @@ class Inbox(TestCase): follow = models.UserFollows.objects.get(user_object=self.local_user) self.assertEqual(follow.user_subject, self.remote_user) - def test_handle_follow_manually_approved(self): - ''' needs approval before following ''' + """ needs approval before following """ activity = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/users/rat/follows/123", "type": "Follow", "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" + "object": "https://example.com/user/mouse", } self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.inbox.activity_task(activity) # notification created notification = models.Notification.objects.get() self.assertEqual(notification.user, self.local_user) - self.assertEqual(notification.notification_type, 'FOLLOW_REQUEST') + self.assertEqual(notification.notification_type, "FOLLOW_REQUEST") # the request should exist request = models.UserFollowRequest.objects.get() @@ -295,38 +275,36 @@ class Inbox(TestCase): follow = models.UserFollows.objects.all() self.assertEqual(list(follow), []) - def test_handle_unfollow(self): - ''' remove a relationship ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ remove a relationship """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): rel = models.UserFollows.objects.create( - user_subject=self.remote_user, user_object=self.local_user) + user_subject=self.remote_user, user_object=self.local_user + ) activity = { "type": "Undo", "id": "bleh", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://example.com/user/mouse/followers"], - 'actor': self.remote_user.remote_id, + "actor": self.remote_user.remote_id, "@context": "https://www.w3.org/ns/activitystreams", "object": { "id": rel.remote_id, "type": "Follow", "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" - } + "object": "https://example.com/user/mouse", + }, } self.assertEqual(self.remote_user, self.local_user.followers.first()) views.inbox.activity_task(activity) self.assertIsNone(self.local_user.followers.first()) - def test_handle_follow_accept(self): - ''' a remote user approved a follow request from local ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ a remote user approved a follow request from local """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): rel = models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user + user_subject=self.local_user, user_object=self.remote_user ) activity = { "@context": "https://www.w3.org/ns/activitystreams", @@ -337,8 +315,8 @@ class Inbox(TestCase): "id": rel.remote_id, "type": "Follow", "actor": "https://example.com/user/mouse", - "object": "https://example.com/users/rat" - } + "object": "https://example.com/users/rat", + }, } self.assertEqual(models.UserFollowRequest.objects.count(), 1) @@ -353,13 +331,11 @@ class Inbox(TestCase): self.assertEqual(follows.count(), 1) self.assertEqual(follows.first(), self.local_user) - def test_handle_follow_reject(self): - ''' turn down a follow request ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ turn down a follow request """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): rel = models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user + user_subject=self.local_user, user_object=self.remote_user ) activity = { "@context": "https://www.w3.org/ns/activitystreams", @@ -370,8 +346,8 @@ class Inbox(TestCase): "id": rel.remote_id, "type": "Follow", "actor": "https://example.com/user/mouse", - "object": "https://example.com/users/rat" - } + "object": "https://example.com/users/rat", + }, } self.assertEqual(models.UserFollowRequest.objects.count(), 1) @@ -382,18 +358,19 @@ class Inbox(TestCase): self.assertFalse(models.UserFollowRequest.objects.exists()) self.assertFalse(self.remote_user.followers.exists()) - def test_handle_update_list(self): - ''' a new list ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ a new list """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): book_list = models.List.objects.create( - name='hi', remote_id='https://example.com/list/22', - user=self.local_user) + name="hi", remote_id="https://example.com/list/22", user=self.local_user + ) activity = { - 'type': 'Update', - 'to': [], 'cc': [], 'actor': 'hi', - 'id': 'sdkjf', - 'object': { + "type": "Update", + "to": [], + "cc": [], + "actor": "hi", + "id": "sdkjf", + "object": { "id": "https://example.com/list/22", "type": "BookList", "totalItems": 1, @@ -401,38 +378,33 @@ class Inbox(TestCase): "last": "https://example.com/list/22?page=1", "name": "Test List", "owner": "https://example.com/user/mouse", - "to": [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "cc": [ - "https://example.com/user/mouse/followers" - ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], "summary": "summary text", "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams" - } + "@context": "https://www.w3.org/ns/activitystreams", + }, } views.inbox.activity_task(activity) book_list.refresh_from_db() - self.assertEqual(book_list.name, 'Test List') - self.assertEqual(book_list.curation, 'curated') - self.assertEqual(book_list.description, 'summary text') - self.assertEqual(book_list.remote_id, 'https://example.com/list/22') - + self.assertEqual(book_list.name, "Test List") + self.assertEqual(book_list.curation, "curated") + self.assertEqual(book_list.description, "summary text") + self.assertEqual(book_list.remote_id, "https://example.com/list/22") def test_handle_delete_status(self): - ''' remove a status ''' + """ remove a status """ self.status.user = self.remote_user self.status.save(broadcast=False) self.assertFalse(self.status.deleted) activity = { - 'type': 'Delete', + "type": "Delete", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://example.com/user/mouse/followers"], - 'id': '%s/activity' % self.status.remote_id, - 'actor': self.remote_user.remote_id, - 'object': {'id': self.status.remote_id, 'type': 'Tombstone'}, + "id": "%s/activity" % self.status.remote_id, + "actor": self.remote_user.remote_id, + "object": {"id": self.status.remote_id, "type": "Tombstone"}, } views.inbox.activity_task(activity) # deletion doens't remove the status, it turns it into a tombstone @@ -440,30 +412,28 @@ class Inbox(TestCase): self.assertTrue(status.deleted) self.assertIsInstance(status.deleted_date, datetime) - def test_handle_delete_status_notifications(self): - ''' remove a status with related notifications ''' + """ remove a status with related notifications """ self.status.user = self.remote_user self.status.save(broadcast=False) models.Notification.objects.create( related_status=self.status, user=self.local_user, - notification_type='MENTION' + notification_type="MENTION", ) # this one is innocent, don't delete it notif = models.Notification.objects.create( - user=self.local_user, - notification_type='MENTION' + user=self.local_user, notification_type="MENTION" ) self.assertFalse(self.status.deleted) self.assertEqual(models.Notification.objects.count(), 2) activity = { - 'type': 'Delete', + "type": "Delete", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://example.com/user/mouse/followers"], - 'id': '%s/activity' % self.status.remote_id, - 'actor': self.remote_user.remote_id, - 'object': {'id': self.status.remote_id, 'type': 'Tombstone'}, + "id": "%s/activity" % self.status.remote_id, + "actor": self.remote_user.remote_id, + "object": {"id": self.status.remote_id, "type": "Tombstone"}, } views.inbox.activity_task(activity) # deletion doens't remove the status, it turns it into a tombstone @@ -475,34 +445,33 @@ class Inbox(TestCase): self.assertEqual(models.Notification.objects.count(), 1) self.assertEqual(models.Notification.objects.get(), notif) - def test_handle_favorite(self): - ''' fav a status ''' + """ fav a status """ activity = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'https://example.com/fav/1', - 'actor': 'https://example.com/users/rat', - 'type': 'Like', - 'published': 'Mon, 25 May 2020 19:31:20 GMT', - 'object': self.status.remote_id, + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/fav/1", + "actor": "https://example.com/users/rat", + "type": "Like", + "published": "Mon, 25 May 2020 19:31:20 GMT", + "object": self.status.remote_id, } views.inbox.activity_task(activity) - fav = models.Favorite.objects.get(remote_id='https://example.com/fav/1') + fav = models.Favorite.objects.get(remote_id="https://example.com/fav/1") self.assertEqual(fav.status, self.status) - self.assertEqual(fav.remote_id, 'https://example.com/fav/1') + self.assertEqual(fav.remote_id, "https://example.com/fav/1") self.assertEqual(fav.user, self.remote_user) def test_ignore_favorite(self): - ''' don't try to save an unknown status ''' + """ don't try to save an unknown status """ activity = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'https://example.com/fav/1', - 'actor': 'https://example.com/users/rat', - 'type': 'Like', - 'published': 'Mon, 25 May 2020 19:31:20 GMT', - 'object': 'https://unknown.status/not-found', + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/fav/1", + "actor": "https://example.com/users/rat", + "type": "Like", + "published": "Mon, 25 May 2020 19:31:20 GMT", + "object": "https://unknown.status/not-found", } views.inbox.activity_task(activity) @@ -510,43 +479,42 @@ class Inbox(TestCase): self.assertFalse(models.Favorite.objects.exists()) def test_handle_unfavorite(self): - ''' fav a status ''' + """ fav a status """ activity = { - 'id': 'https://example.com/fav/1#undo', - 'type': 'Undo', + "id": "https://example.com/fav/1#undo", + "type": "Undo", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://example.com/user/mouse/followers"], - 'actor': self.remote_user.remote_id, - 'object': { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'https://example.com/fav/1', - 'actor': 'https://example.com/users/rat', - 'type': 'Like', - 'published': 'Mon, 25 May 2020 19:31:20 GMT', - 'object': self.status.remote_id, - } + "actor": self.remote_user.remote_id, + "object": { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/fav/1", + "actor": "https://example.com/users/rat", + "type": "Like", + "published": "Mon, 25 May 2020 19:31:20 GMT", + "object": self.status.remote_id, + }, } models.Favorite.objects.create( status=self.status, user=self.remote_user, - remote_id='https://example.com/fav/1') + remote_id="https://example.com/fav/1", + ) self.assertEqual(models.Favorite.objects.count(), 1) views.inbox.activity_task(activity) self.assertEqual(models.Favorite.objects.count(), 0) - def test_handle_boost(self): - ''' boost a status ''' + """ boost a status """ self.assertEqual(models.Notification.objects.count(), 0) activity = { - 'type': 'Announce', - 'id': '%s/boost' % self.status.remote_id, - 'actor': self.remote_user.remote_id, - 'object': self.status.remote_id, + "type": "Announce", + "id": "%s/boost" % self.status.remote_id, + "actor": self.remote_user.remote_id, + "object": self.status.remote_id, } - with patch('bookwyrm.models.status.Status.ignore_activity') \ - as discarder: + with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: discarder.return_value = False views.inbox.activity_task(activity) boost = models.Boost.objects.get() @@ -555,59 +523,56 @@ class Inbox(TestCase): self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.related_status, self.status) - @responses.activate def test_handle_discarded_boost(self): - ''' test a boost of a mastodon status that will be discarded ''' + """ test a boost of a mastodon status that will be discarded """ status = models.Status( - content='hi', + content="hi", user=self.remote_user, ) status.save(broadcast=False) activity = { - 'type': 'Announce', - 'id': 'http://www.faraway.com/boost/12', - 'actor': self.remote_user.remote_id, - 'object': status.remote_id, + "type": "Announce", + "id": "http://www.faraway.com/boost/12", + "actor": self.remote_user.remote_id, + "object": status.remote_id, } responses.add( - responses.GET, - status.remote_id, - json=status.to_activity(), - status=200) + responses.GET, status.remote_id, json=status.to_activity(), status=200 + ) views.inbox.activity_task(activity) self.assertEqual(models.Boost.objects.count(), 0) - def test_handle_unboost(self): - ''' undo a boost ''' + """ undo a boost """ boost = models.Boost.objects.create( - boosted_status=self.status, user=self.remote_user) + boosted_status=self.status, user=self.remote_user + ) activity = { - 'type': 'Undo', - 'actor': 'hi', - 'id': 'bleh', + "type": "Undo", + "actor": "hi", + "id": "bleh", "to": ["https://www.w3.org/ns/activitystreams#public"], "cc": ["https://example.com/user/mouse/followers"], - 'object': { - 'type': 'Announce', - 'id': boost.remote_id, - 'actor': self.remote_user.remote_id, - 'object': self.status.remote_id, - } + "object": { + "type": "Announce", + "id": boost.remote_id, + "actor": self.remote_user.remote_id, + "object": self.status.remote_id, + }, } views.inbox.activity_task(activity) - def test_handle_add_book_to_shelf(self): - ''' shelving a book ''' - work = models.Work.objects.create(title='work title') + """ shelving a book """ + work = models.Work.objects.create(title="work title") book = models.Edition.objects.create( - title='Test', remote_id='https://bookwyrm.social/book/37292', - parent_work=work) - shelf = models.Shelf.objects.create( - user=self.remote_user, name='Test Shelf') - shelf.remote_id = 'https://bookwyrm.social/user/mouse/shelf/to-read' + title="Test", + remote_id="https://bookwyrm.social/book/37292", + parent_work=work, + ) + shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf") + shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read" shelf.save() activity = { @@ -621,23 +586,24 @@ class Inbox(TestCase): "id": "https://bookwyrm.social/book/37292", }, "target": "https://bookwyrm.social/user/mouse/shelf/to-read", - "@context": "https://www.w3.org/ns/activitystreams" + "@context": "https://www.w3.org/ns/activitystreams", } views.inbox.activity_task(activity) self.assertEqual(shelf.books.first(), book) - @responses.activate def test_handle_add_book_to_list(self): - ''' listing a book ''' - work = models.Work.objects.create(title='work title') + """ listing a book """ + work = models.Work.objects.create(title="work title") book = models.Edition.objects.create( - title='Test', remote_id='https://bookwyrm.social/book/37292', - parent_work=work) + title="Test", + remote_id="https://bookwyrm.social/book/37292", + parent_work=work, + ) responses.add( responses.GET, - 'https://bookwyrm.social/user/mouse/list/to-read', + "https://bookwyrm.social/user/mouse/list/to-read", json={ "id": "https://example.com/list/22", "type": "BookList", @@ -646,16 +612,12 @@ class Inbox(TestCase): "last": "https://example.com/list/22?page=1", "name": "Test List", "owner": "https://example.com/user/mouse", - "to": [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "cc": [ - "https://example.com/user/mouse/followers" - ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], "summary": "summary text", "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams" - } + "@context": "https://www.w3.org/ns/activitystreams", + }, ) activity = { @@ -669,26 +631,27 @@ class Inbox(TestCase): "id": "https://bookwyrm.social/book/37292", }, "target": "https://bookwyrm.social/user/mouse/list/to-read", - "@context": "https://www.w3.org/ns/activitystreams" + "@context": "https://www.w3.org/ns/activitystreams", } views.inbox.activity_task(activity) booklist = models.List.objects.get() - self.assertEqual(booklist.name, 'Test List') + self.assertEqual(booklist.name, "Test List") self.assertEqual(booklist.books.first(), book) - @responses.activate def test_handle_tag_book(self): - ''' listing a book ''' - work = models.Work.objects.create(title='work title') + """ listing a book """ + work = models.Work.objects.create(title="work title") book = models.Edition.objects.create( - title='Test', remote_id='https://bookwyrm.social/book/37292', - parent_work=work) + title="Test", + remote_id="https://bookwyrm.social/book/37292", + parent_work=work, + ) responses.add( responses.GET, - 'https://www.example.com/tag/cool-tag', + "https://www.example.com/tag/cool-tag", json={ "id": "https://1b1a78582461.ngrok.io/tag/tag", "type": "OrderedCollection", @@ -696,8 +659,8 @@ class Inbox(TestCase): "first": "https://1b1a78582461.ngrok.io/tag/tag?page=1", "last": "https://1b1a78582461.ngrok.io/tag/tag?page=1", "name": "cool tag", - "@context": "https://www.w3.org/ns/activitystreams" - } + "@context": "https://www.w3.org/ns/activitystreams", + }, ) activity = { @@ -711,95 +674,101 @@ class Inbox(TestCase): "id": "https://bookwyrm.social/book/37292", }, "target": "https://www.example.com/tag/cool-tag", - "@context": "https://www.w3.org/ns/activitystreams" + "@context": "https://www.w3.org/ns/activitystreams", } views.inbox.activity_task(activity) tag = models.Tag.objects.get() self.assertFalse(models.List.objects.exists()) - self.assertEqual(tag.name, 'cool tag') + self.assertEqual(tag.name, "cool tag") self.assertEqual(tag.books.first(), book) - def test_handle_update_user(self): - ''' update an existing user ''' + """ update an existing user """ # we only do this with remote users self.local_user.local = False self.local_user.save() - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/ap_user.json') + datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) - del userdata['icon'] + del userdata["icon"] self.assertIsNone(self.local_user.name) - views.inbox.activity_task({ - 'type': 'Update', - 'to': [], 'cc': [], 'actor': 'hi', - 'id': 'sdkjf', - 'object': userdata - }) + views.inbox.activity_task( + { + "type": "Update", + "to": [], + "cc": [], + "actor": "hi", + "id": "sdkjf", + "object": userdata, + } + ) user = models.User.objects.get(id=self.local_user.id) - self.assertEqual(user.name, 'MOUSE?? MOUSE!!') - self.assertEqual(user.username, 'mouse@example.com') - self.assertEqual(user.localname, 'mouse') - + self.assertEqual(user.name, "MOUSE?? MOUSE!!") + self.assertEqual(user.username, "mouse@example.com") + self.assertEqual(user.localname, "mouse") def test_handle_update_edition(self): - ''' update an existing edition ''' - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/bw_edition.json') + """ update an existing edition """ + datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_edition.json") bookdata = json.loads(datafile.read_bytes()) models.Work.objects.create( - title='Test Work', remote_id='https://bookwyrm.social/book/5988') + title="Test Work", remote_id="https://bookwyrm.social/book/5988" + ) book = models.Edition.objects.create( - title='Test Book', remote_id='https://bookwyrm.social/book/5989') + title="Test Book", remote_id="https://bookwyrm.social/book/5989" + ) - del bookdata['authors'] - self.assertEqual(book.title, 'Test Book') + del bookdata["authors"] + self.assertEqual(book.title, "Test Book") - with patch( - 'bookwyrm.activitypub.base_activity.set_related_field.delay'): - views.inbox.activity_task({ - 'type': 'Update', - 'to': [], 'cc': [], 'actor': 'hi', - 'id': 'sdkjf', - 'object': bookdata - }) + with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"): + views.inbox.activity_task( + { + "type": "Update", + "to": [], + "cc": [], + "actor": "hi", + "id": "sdkjf", + "object": bookdata, + } + ) book = models.Edition.objects.get(id=book.id) - self.assertEqual(book.title, 'Piranesi') - + self.assertEqual(book.title, "Piranesi") def test_handle_update_work(self): - ''' update an existing edition ''' - datafile = pathlib.Path(__file__).parent.joinpath( - '../data/bw_work.json') + """ update an existing edition """ + datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_work.json") bookdata = json.loads(datafile.read_bytes()) book = models.Work.objects.create( - title='Test Book', remote_id='https://bookwyrm.social/book/5988') + title="Test Book", remote_id="https://bookwyrm.social/book/5988" + ) - del bookdata['authors'] - self.assertEqual(book.title, 'Test Book') - with patch( - 'bookwyrm.activitypub.base_activity.set_related_field.delay'): - views.inbox.activity_task({ - 'type': 'Update', - 'to': [], 'cc': [], 'actor': 'hi', - 'id': 'sdkjf', - 'object': bookdata - }) + del bookdata["authors"] + self.assertEqual(book.title, "Test Book") + with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"): + views.inbox.activity_task( + { + "type": "Update", + "to": [], + "cc": [], + "actor": "hi", + "id": "sdkjf", + "object": bookdata, + } + ) book = models.Work.objects.get(id=book.id) - self.assertEqual(book.title, 'Piranesi') - + self.assertEqual(book.title, "Piranesi") def test_handle_blocks(self): - ''' create a "block" database entry from an activity ''' + """ create a "block" database entry from an activity """ self.local_user.followers.add(self.remote_user) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user) + user_subject=self.local_user, user_object=self.remote_user + ) self.assertTrue(models.UserFollows.objects.exists()) self.assertTrue(models.UserFollowRequest.objects.exists()) @@ -808,42 +777,41 @@ class Inbox(TestCase): "id": "https://example.com/9e1f41ac-9ddd-4159", "type": "Block", "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" + "object": "https://example.com/user/mouse", } views.inbox.activity_task(activity) block = models.UserBlocks.objects.get() self.assertEqual(block.user_subject, self.remote_user) self.assertEqual(block.user_object, self.local_user) - self.assertEqual( - block.remote_id, 'https://example.com/9e1f41ac-9ddd-4159') + self.assertEqual(block.remote_id, "https://example.com/9e1f41ac-9ddd-4159") self.assertFalse(models.UserFollows.objects.exists()) self.assertFalse(models.UserFollowRequest.objects.exists()) - def test_handle_unblock(self): - ''' unblock a user ''' + """ unblock a user """ self.remote_user.blocks.add(self.local_user) block = models.UserBlocks.objects.get() - block.remote_id = 'https://example.com/9e1f41ac-9ddd-4159' + block.remote_id = "https://example.com/9e1f41ac-9ddd-4159" block.save() self.assertEqual(block.user_subject, self.remote_user) self.assertEqual(block.user_object, self.local_user) activity = { - 'type': 'Undo', - 'actor': 'hi', - 'id': 'bleh', + "type": "Undo", + "actor": "hi", + "id": "bleh", "to": ["https://www.w3.org/ns/activitystreams#public"], "cc": ["https://example.com/user/mouse/followers"], - 'object': { + "object": { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/9e1f41ac-9ddd-4159", "type": "Block", "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" - }} + "object": "https://example.com/user/mouse", + }, + } views.inbox.activity_task(activity) self.assertFalse(models.UserBlocks.objects.exists()) diff --git a/bookwyrm/tests/views/test_interaction.py b/bookwyrm/tests/views/test_interaction.py index c6d39f29a..857f7061f 100644 --- a/bookwyrm/tests/views/test_interaction.py +++ b/bookwyrm/tests/views/test_interaction.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.test import TestCase from django.test.client import RequestFactory @@ -7,40 +7,44 @@ from bookwyrm import models, views class InteractionViews(TestCase): - ''' viewing and creating statuses ''' + """ viewing and creating statuses """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - with patch('bookwyrm.models.user.set_remote_server'): + with patch("bookwyrm.models.user.set_remote_server"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@email.com', 'ratword', + "rat", + "rat@email.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) - work = models.Work.objects.create(title='Test Work') + work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=work, ) - def test_handle_favorite(self): - ''' create and broadcast faving a status ''' + """ create and broadcast faving a status """ view = views.Favorite.as_view() - request = self.factory.post('') + request = self.factory.post("") request.user = self.remote_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create( - user=self.local_user, content='hi') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(user=self.local_user, content="hi") view(request, status.id) fav = models.Favorite.objects.get() @@ -48,106 +52,100 @@ class InteractionViews(TestCase): self.assertEqual(fav.user, self.remote_user) notification = models.Notification.objects.get() - self.assertEqual(notification.notification_type, 'FAVORITE') + self.assertEqual(notification.notification_type, "FAVORITE") self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.related_user, self.remote_user) - def test_handle_unfavorite(self): - ''' unfav a status ''' + """ unfav a status """ view = views.Unfavorite.as_view() - request = self.factory.post('') + request = self.factory.post("") request.user = self.remote_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create( - user=self.local_user, content='hi') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(user=self.local_user, content="hi") views.Favorite.as_view()(request, status.id) self.assertEqual(models.Favorite.objects.count(), 1) self.assertEqual(models.Notification.objects.count(), 1) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): view(request, status.id) self.assertEqual(models.Favorite.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0) - def test_handle_boost(self): - ''' boost a status ''' + """ boost a status """ view = views.Boost.as_view() - request = self.factory.post('') + request = self.factory.post("") request.user = self.remote_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create( - user=self.local_user, content='hi') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(user=self.local_user, content="hi") view(request, status.id) boost = models.Boost.objects.get() self.assertEqual(boost.boosted_status, status) self.assertEqual(boost.user, self.remote_user) - self.assertEqual(boost.privacy, 'public') + self.assertEqual(boost.privacy, "public") notification = models.Notification.objects.get() - self.assertEqual(notification.notification_type, 'BOOST') + self.assertEqual(notification.notification_type, "BOOST") self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.related_user, self.remote_user) self.assertEqual(notification.related_status, status) def test_handle_boost_unlisted(self): - ''' boost a status ''' + """ boost a status """ view = views.Boost.as_view() - request = self.factory.post('') + request = self.factory.post("") request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( - user=self.local_user, content='hi', privacy='unlisted') + user=self.local_user, content="hi", privacy="unlisted" + ) view(request, status.id) boost = models.Boost.objects.get() - self.assertEqual(boost.privacy, 'unlisted') + self.assertEqual(boost.privacy, "unlisted") def test_handle_boost_private(self): - ''' boost a status ''' + """ boost a status """ view = views.Boost.as_view() - request = self.factory.post('') + request = self.factory.post("") request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( - user=self.local_user, content='hi', privacy='followers') + user=self.local_user, content="hi", privacy="followers" + ) view(request, status.id) self.assertFalse(models.Boost.objects.exists()) def test_handle_boost_twice(self): - ''' boost a status ''' + """ boost a status """ view = views.Boost.as_view() - request = self.factory.post('') + request = self.factory.post("") request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create( - user=self.local_user, content='hi') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(user=self.local_user, content="hi") view(request, status.id) view(request, status.id) self.assertEqual(models.Boost.objects.count(), 1) - def test_handle_unboost(self): - ''' undo a boost ''' + """ undo a boost """ view = views.Unboost.as_view() - request = self.factory.post('') + request = self.factory.post("") request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create( - user=self.local_user, content='hi') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(user=self.local_user, content="hi") views.Boost.as_view()(request, status.id) self.assertEqual(models.Boost.objects.count(), 1) self.assertEqual(models.Notification.objects.count(), 1) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ - as mock: + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: view(request, status.id) self.assertEqual(mock.call_count, 1) self.assertEqual(models.Boost.objects.count(), 0) diff --git a/bookwyrm/tests/views/test_invite.py b/bookwyrm/tests/views/test_invite.py index e93e7209b..cd2276c0d 100644 --- a/bookwyrm/tests/views/test_invite.py +++ b/bookwyrm/tests/views/test_invite.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.contrib.auth.models import AnonymousUser @@ -11,36 +11,39 @@ from bookwyrm import views class InviteViews(TestCase): - ''' every response to a get request, html or json ''' + """ every response to a get request, html or json """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ 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') + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) models.SiteSettings.objects.create() - def test_invite_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Invite.as_view() - models.SiteInvite.objects.create(code='hi', user=self.local_user) - request = self.factory.get('') + models.SiteInvite.objects.create(code="hi", user=self.local_user) + request = self.factory.get("") request.user = AnonymousUser # why?? this is annoying. request.user.is_authenticated = False - with patch('bookwyrm.models.site.SiteInvite.valid') as invite: + with patch("bookwyrm.models.site.SiteInvite.valid") as invite: invite.return_value = True - result = view(request, 'hi') + result = view(request, "hi") self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - def test_manage_invites(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.ManageInvites.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True result = view(request) diff --git a/bookwyrm/tests/views/test_isbn.py b/bookwyrm/tests/views/test_isbn.py index 1966702b4..c7ae1f39f 100644 --- a/bookwyrm/tests/views/test_isbn.py +++ b/bookwyrm/tests/views/test_isbn.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ import json from unittest.mock import patch @@ -13,42 +13,41 @@ from bookwyrm.settings import DOMAIN class IsbnViews(TestCase): - ''' tag views''' + """ tag views""" + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Test Book', - isbn_13='1234567890123', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Test Book", + isbn_13="1234567890123", + remote_id="https://example.com/book/1", + parent_work=self.work, ) models.Connector.objects.create( - identifier='self', - connector_file='self_connector', - local=True + identifier="self", connector_file="self_connector", local=True ) models.SiteSettings.objects.create() - def test_isbn_json_response(self): - ''' searches local data only and returns book data in json format ''' + """ searches local data only and returns book data in json format """ view = views.Isbn.as_view() - request = self.factory.get('') - with patch('bookwyrm.views.isbn.is_api_request') as is_api: + request = self.factory.get("") + with patch("bookwyrm.views.isbn.is_api_request") as is_api: is_api.return_value = True - response = view(request, isbn='1234567890123') + response = view(request, isbn="1234567890123") self.assertIsInstance(response, JsonResponse) data = json.loads(response.content) self.assertEqual(len(data), 1) - self.assertEqual(data[0]['title'], 'Test Book') - self.assertEqual( - data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id)) - + self.assertEqual(data[0]["title"], "Test Book") + self.assertEqual(data[0]["key"], "https://%s/book/%d" % (DOMAIN, self.book.id)) diff --git a/bookwyrm/tests/views/test_landing.py b/bookwyrm/tests/views/test_landing.py index 5e0e50cff..910e4a851 100644 --- a/bookwyrm/tests/views/test_landing.py +++ b/bookwyrm/tests/views/test_landing.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from django.contrib.auth.models import AnonymousUser from django.template.response import TemplateResponse from django.test import TestCase @@ -9,22 +9,26 @@ from bookwyrm import views class LandingViews(TestCase): - ''' pages you land on without really trying ''' + """ pages you land on without really trying """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ 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') + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False models.SiteSettings.objects.create() - def test_home_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Home.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user result = view(request) self.assertEqual(result.status_code, 200) @@ -36,21 +40,19 @@ class LandingViews(TestCase): self.assertEqual(result.status_code, 200) result.render() - def test_about_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.About.as_view() - request = self.factory.get('') + 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_discover(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Discover.as_view() - request = self.factory.get('') + request = self.factory.get("") result = view(request) self.assertIsInstance(result, TemplateResponse) diff --git a/bookwyrm/tests/views/test_list.py b/bookwyrm/tests/views/test_list.py index e41d9806c..cc895ad17 100644 --- a/bookwyrm/tests/views/test_list.py +++ b/bookwyrm/tests/views/test_list.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.contrib.auth.models import AnonymousUser @@ -9,44 +9,52 @@ from django.test.client import RequestFactory from bookwyrm import models, views from bookwyrm.activitypub import ActivitypubResponse -#pylint: disable=unused-argument +# pylint: disable=unused-argument class ListViews(TestCase): - ''' tag views''' + """ tag views""" + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) self.rat = models.User.objects.create_user( - 'rat@local.com', 'rat@rat.com', 'ratword', - local=True, localname='rat', - remote_id='https://example.com/users/rat', + "rat@local.com", + "rat@rat.com", + "ratword", + local=True, + localname="rat", + remote_id="https://example.com/users/rat", ) - work = models.Work.objects.create(title='Work') + work = models.Work.objects.create(title="Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', + title="Example Edition", + remote_id="https://example.com/book/1", parent_work=work, ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): self.list = models.List.objects.create( - name='Test List', user=self.local_user) + name="Test List", user=self.local_user + ) self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False models.SiteSettings.objects.create() - def test_lists_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Lists.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.List.objects.create(name='Public list', user=self.local_user) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.List.objects.create(name="Public list", user=self.local_user) models.List.objects.create( - name='Private list', privacy='direct', user=self.local_user) - request = self.factory.get('') + name="Private list", privacy="direct", user=self.local_user + ) + request = self.factory.get("") request.user = self.local_user result = view(request) @@ -61,42 +69,45 @@ class ListViews(TestCase): result.render() self.assertEqual(result.status_code, 200) - def test_lists_create(self): - ''' create list view ''' + """ create list view """ real_broadcast = models.List.broadcast + def mock_broadcast(_, activity, user, **kwargs): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Create') - self.assertEqual(activity['actor'], self.local_user.remote_id) + self.assertEqual(activity["type"], "Create") + self.assertEqual(activity["actor"], self.local_user.remote_id) + models.List.broadcast = mock_broadcast view = views.Lists.as_view() - request = self.factory.post('', { - 'name': 'A list', - 'description': 'wow', - 'privacy': 'unlisted', - 'curation': 'open', - 'user': self.local_user.id, - }) + request = self.factory.post( + "", + { + "name": "A list", + "description": "wow", + "privacy": "unlisted", + "curation": "open", + "user": self.local_user.id, + }, + ) request.user = self.local_user result = view(request) self.assertEqual(result.status_code, 302) - new_list = models.List.objects.filter(name='A list').get() - self.assertEqual(new_list.description, 'wow') - self.assertEqual(new_list.privacy, 'unlisted') - self.assertEqual(new_list.curation, 'open') + new_list = models.List.objects.filter(name="A list").get() + self.assertEqual(new_list.description, "wow") + self.assertEqual(new_list.privacy, "unlisted") + self.assertEqual(new_list.curation, "open") models.List.broadcast = real_broadcast - def test_list_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.List.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.views.list.is_api_request') as is_api: + with patch("bookwyrm.views.list.is_api_request") as is_api: is_api.return_value = False result = view(request, self.list.id) self.assertIsInstance(result, TemplateResponse) @@ -104,68 +115,72 @@ class ListViews(TestCase): self.assertEqual(result.status_code, 200) request.user = self.anonymous_user - with patch('bookwyrm.views.list.is_api_request') as is_api: + with patch("bookwyrm.views.list.is_api_request") as is_api: is_api.return_value = False result = view(request, self.list.id) self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - with patch('bookwyrm.views.list.is_api_request') as is_api: + with patch("bookwyrm.views.list.is_api_request") as is_api: is_api.return_value = True result = view(request, self.list.id) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - request = self.factory.get('/?page=1') + request = self.factory.get("/?page=1") request.user = self.local_user - with patch('bookwyrm.views.list.is_api_request') as is_api: + with patch("bookwyrm.views.list.is_api_request") as is_api: is_api.return_value = True result = view(request, self.list.id) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_list_edit(self): - ''' edit a list ''' + """ edit a list """ real_broadcast = models.List.broadcast + def mock_broadcast(_, activity, user, **kwargs): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Update') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['object']['id'], self.list.remote_id) + self.assertEqual(activity["type"], "Update") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["id"], self.list.remote_id) + models.List.broadcast = mock_broadcast view = views.List.as_view() - request = self.factory.post('', { - 'name': 'New Name', - 'description': 'wow', - 'privacy': 'direct', - 'curation': 'curated', - 'user': self.local_user.id, - }) + request = self.factory.post( + "", + { + "name": "New Name", + "description": "wow", + "privacy": "direct", + "curation": "curated", + "user": self.local_user.id, + }, + ) request.user = self.local_user result = view(request, self.list.id) self.assertEqual(result.status_code, 302) self.list.refresh_from_db() - self.assertEqual(self.list.name, 'New Name') - self.assertEqual(self.list.description, 'wow') - self.assertEqual(self.list.privacy, 'direct') - self.assertEqual(self.list.curation, 'curated') + self.assertEqual(self.list.name, "New Name") + self.assertEqual(self.list.description, "wow") + self.assertEqual(self.list.privacy, "direct") + self.assertEqual(self.list.curation, "curated") models.List.broadcast = real_broadcast - def test_curate_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Curate.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.List.objects.create(name='Public list', user=self.local_user) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.List.objects.create(name="Public list", user=self.local_user) models.List.objects.create( - name='Private list', privacy='direct', user=self.local_user) - request = self.factory.get('') + name="Private list", privacy="direct", user=self.local_user + ) + request = self.factory.get("") request.user = self.local_user result = view(request, self.list.id) @@ -177,31 +192,35 @@ class ListViews(TestCase): result = view(request, self.list.id) self.assertEqual(result.status_code, 302) - def test_curate_approve(self): - ''' approve a pending item ''' + """ approve a pending item """ real_broadcast = models.List.broadcast + def mock_broadcast(_, activity, user, **kwargs): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Add') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['target'], self.list.remote_id) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + models.ListItem.broadcast = mock_broadcast view = views.Curate.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): pending = models.ListItem.objects.create( book_list=self.list, user=self.local_user, book=self.book, - approved=False + approved=False, ) - request = self.factory.post('', { - 'item': pending.id, - 'approved': 'true', - }) + request = self.factory.post( + "", + { + "item": pending.id, + "approved": "true", + }, + ) request.user = self.local_user view(request, self.list.id) @@ -211,43 +230,49 @@ class ListViews(TestCase): self.assertTrue(pending.approved) models.ListItem.broadcast = real_broadcast - def test_curate_reject(self): - ''' approve a pending item ''' + """ approve a pending item """ view = views.Curate.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): pending = models.ListItem.objects.create( book_list=self.list, user=self.local_user, book=self.book, - approved=False + approved=False, ) - request = self.factory.post('', { - 'item': pending.id, - 'approved': 'false', - }) + request = self.factory.post( + "", + { + "item": pending.id, + "approved": "false", + }, + ) 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.list.id) self.assertFalse(self.list.books.exists()) self.assertFalse(models.ListItem.objects.exists()) - def test_add_book(self): - ''' put a book on a list ''' + """ put a book on a list """ real_broadcast = models.List.broadcast + def mock_broadcast(_, activity, user): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Add') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['target'], self.list.remote_id) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + models.ListItem.broadcast = mock_broadcast - request = self.factory.post('', { - 'book': self.book.id, - }) + request = self.factory.post( + "", + { + "book": self.book.id, + }, + ) request.user = self.local_user views.list.add_book(request, self.list.id) @@ -257,22 +282,26 @@ class ListViews(TestCase): self.assertTrue(item.approved) models.ListItem.broadcast = real_broadcast - def test_add_book_outsider(self): - ''' put a book on a list ''' + """ put a book on a list """ real_broadcast = models.List.broadcast + def mock_broadcast(_, activity, user): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.rat.remote_id) - self.assertEqual(activity['type'], 'Add') - self.assertEqual(activity['actor'], self.rat.remote_id) - self.assertEqual(activity['target'], self.list.remote_id) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.rat.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + models.ListItem.broadcast = mock_broadcast - self.list.curation = 'open' + self.list.curation = "open" self.list.save(broadcast=False) - request = self.factory.post('', { - 'book': self.book.id, - }) + request = self.factory.post( + "", + { + "book": self.book.id, + }, + ) request.user = self.rat views.list.add_book(request, self.list.id) @@ -282,23 +311,27 @@ class ListViews(TestCase): self.assertTrue(item.approved) models.ListItem.broadcast = real_broadcast - def test_add_book_pending(self): - ''' put a book on a list awaiting approval ''' + """ put a book on a list awaiting approval """ real_broadcast = models.List.broadcast + def mock_broadcast(_, activity, user): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.rat.remote_id) - self.assertEqual(activity['type'], 'Add') - self.assertEqual(activity['actor'], self.rat.remote_id) - self.assertEqual(activity['target'], self.list.remote_id) - self.assertEqual(activity['object']['id'], self.book.remote_id) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.rat.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + self.assertEqual(activity["object"]["id"], self.book.remote_id) + models.ListItem.broadcast = mock_broadcast - self.list.curation = 'curated' + self.list.curation = "curated" self.list.save(broadcast=False) - request = self.factory.post('', { - 'book': self.book.id, - }) + request = self.factory.post( + "", + { + "book": self.book.id, + }, + ) request.user = self.rat views.list.add_book(request, self.list.id) @@ -308,23 +341,27 @@ class ListViews(TestCase): self.assertFalse(item.approved) models.ListItem.broadcast = real_broadcast - def test_add_book_self_curated(self): - ''' put a book on a list automatically approved ''' + """ put a book on a list automatically approved """ real_broadcast = models.ListItem.broadcast + def mock_broadcast(_, activity, user): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Add') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['target'], self.list.remote_id) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + models.ListItem.broadcast = mock_broadcast - self.list.curation = 'curated' + self.list.curation = "curated" self.list.save(broadcast=False) - request = self.factory.post('', { - 'book': self.book.id, - }) + request = self.factory.post( + "", + { + "book": self.book.id, + }, + ) request.user = self.local_user views.list.add_book(request, self.list.id) @@ -334,12 +371,11 @@ class ListViews(TestCase): self.assertTrue(item.approved) models.ListItem.broadcast = real_broadcast - def test_remove_book(self): - ''' take an item off a list ''' + """ take an item off a list """ real_broadcast = models.ListItem.broadcast - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): item = models.ListItem.objects.create( book_list=self.list, user=self.local_user, @@ -348,15 +384,19 @@ class ListViews(TestCase): self.assertTrue(self.list.listitem_set.exists()) def mock_broadcast(_, activity, user): - ''' ok ''' + """ ok """ self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity['type'], 'Remove') - self.assertEqual(activity['actor'], self.local_user.remote_id) - self.assertEqual(activity['target'], self.list.remote_id) + self.assertEqual(activity["type"], "Remove") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + models.ListItem.broadcast = mock_broadcast - request = self.factory.post('', { - 'item': item.id, - }) + request = self.factory.post( + "", + { + "item": item.id, + }, + ) request.user = self.local_user views.list.remove_book(request, self.list.id) @@ -364,19 +404,21 @@ class ListViews(TestCase): self.assertFalse(self.list.listitem_set.exists()) models.ListItem.broadcast = real_broadcast - def test_remove_book_unauthorized(self): - ''' take an item off a list ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ take an item off a list """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): item = models.ListItem.objects.create( book_list=self.list, user=self.local_user, book=self.book, ) self.assertTrue(self.list.listitem_set.exists()) - request = self.factory.post('', { - 'item': item.id, - }) + request = self.factory.post( + "", + { + "item": item.id, + }, + ) request.user = self.rat views.list.remove_book(request, self.list.id) diff --git a/bookwyrm/tests/views/test_notifications.py b/bookwyrm/tests/views/test_notifications.py index 555133597..6d92485ef 100644 --- a/bookwyrm/tests/views/test_notifications.py +++ b/bookwyrm/tests/views/test_notifications.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -8,19 +8,24 @@ from bookwyrm import views class NotificationViews(TestCase): - ''' notifications ''' + """ notifications """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ 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') + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) models.SiteSettings.objects.create() def test_notifications_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Notifications.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) @@ -28,14 +33,16 @@ class NotificationViews(TestCase): self.assertEqual(result.status_code, 200) def test_clear_notifications(self): - ''' erase notifications ''' + """ erase notifications """ models.Notification.objects.create( - user=self.local_user, notification_type='FAVORITE') + user=self.local_user, notification_type="FAVORITE" + ) models.Notification.objects.create( - user=self.local_user, notification_type='MENTION', read=True) + user=self.local_user, notification_type="MENTION", read=True + ) self.assertEqual(models.Notification.objects.count(), 2) view = views.Notifications.as_view() - request = self.factory.post('') + request = self.factory.post("") request.user = self.local_user result = view(request) self.assertEqual(result.status_code, 302) diff --git a/bookwyrm/tests/views/test_outbox.py b/bookwyrm/tests/views/test_outbox.py index 7986dea61..5934eb7c7 100644 --- a/bookwyrm/tests/views/test_outbox.py +++ b/bookwyrm/tests/views/test_outbox.py @@ -1,4 +1,4 @@ -''' sending out activities ''' +""" sending out activities """ from unittest.mock import patch import json @@ -12,118 +12,127 @@ from bookwyrm.settings import USER_AGENT # pylint: disable=too-many-public-methods class OutboxView(TestCase): - ''' sends out activities ''' + """ sends out activities """ + def setUp(self): - ''' we'll need some data ''' + """ we'll need some data """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - work = models.Work.objects.create(title='Test Work') + work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=work, ) - def test_outbox(self): - ''' returns user's statuses ''' - request = self.factory.get('') - result = views.Outbox.as_view()(request, 'mouse') + """ returns user's statuses """ + request = self.factory.get("") + result = views.Outbox.as_view()(request, "mouse") self.assertIsInstance(result, JsonResponse) def test_outbox_bad_method(self): - ''' can't POST to outbox ''' - request = self.factory.post('') - result = views.Outbox.as_view()(request, 'mouse') + """ can't POST to outbox """ + request = self.factory.post("") + result = views.Outbox.as_view()(request, "mouse") self.assertEqual(result.status_code, 405) def test_outbox_unknown_user(self): - ''' should 404 for unknown and remote users ''' - request = self.factory.post('') - result = views.Outbox.as_view()(request, 'beepboop') + """ should 404 for unknown and remote users """ + request = self.factory.post("") + result = views.Outbox.as_view()(request, "beepboop") self.assertEqual(result.status_code, 405) - result = views.Outbox.as_view()(request, 'rat') + result = views.Outbox.as_view()(request, "rat") self.assertEqual(result.status_code, 405) def test_outbox_privacy(self): - ''' don't show dms et cetera in outbox ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ don't show dms et cetera in outbox """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.Status.objects.create( - content='PRIVATE!!', user=self.local_user, privacy='direct') + content="PRIVATE!!", user=self.local_user, privacy="direct" + ) models.Status.objects.create( - content='bffs ONLY', user=self.local_user, privacy='followers') + content="bffs ONLY", user=self.local_user, privacy="followers" + ) models.Status.objects.create( - content='unlisted status', user=self.local_user, - privacy='unlisted') + content="unlisted status", user=self.local_user, privacy="unlisted" + ) models.Status.objects.create( - content='look at this', user=self.local_user, privacy='public') + content="look at this", user=self.local_user, privacy="public" + ) - request = self.factory.get('') - result = views.Outbox.as_view()(request, 'mouse') + request = self.factory.get("") + result = views.Outbox.as_view()(request, "mouse") self.assertIsInstance(result, JsonResponse) data = json.loads(result.content) - self.assertEqual(data['type'], 'OrderedCollection') - self.assertEqual(data['totalItems'], 2) + self.assertEqual(data["type"], "OrderedCollection") + self.assertEqual(data["totalItems"], 2) def test_outbox_filter(self): - ''' if we only care about reviews, only get reviews ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ if we only care about reviews, only get reviews """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.Review.objects.create( - content='look at this', name='hi', rating=1, - book=self.book, user=self.local_user) - models.Status.objects.create( - content='look at this', user=self.local_user) + content="look at this", + name="hi", + rating=1, + book=self.book, + user=self.local_user, + ) + models.Status.objects.create(content="look at this", user=self.local_user) - request = self.factory.get('', {'type': 'bleh'}) - result = views.Outbox.as_view()(request, 'mouse') + request = self.factory.get("", {"type": "bleh"}) + result = views.Outbox.as_view()(request, "mouse") self.assertIsInstance(result, JsonResponse) data = json.loads(result.content) - self.assertEqual(data['type'], 'OrderedCollection') - self.assertEqual(data['totalItems'], 2) + self.assertEqual(data["type"], "OrderedCollection") + self.assertEqual(data["totalItems"], 2) - request = self.factory.get('', {'type': 'Review'}) - result = views.Outbox.as_view()(request, 'mouse') + request = self.factory.get("", {"type": "Review"}) + result = views.Outbox.as_view()(request, "mouse") self.assertIsInstance(result, JsonResponse) data = json.loads(result.content) - self.assertEqual(data['type'], 'OrderedCollection') - self.assertEqual(data['totalItems'], 1) + self.assertEqual(data["type"], "OrderedCollection") + self.assertEqual(data["totalItems"], 1) def test_outbox_bookwyrm_request_true(self): - ''' should differentiate between bookwyrm and outside requests ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ should differentiate between bookwyrm and outside requests """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.Review.objects.create( - name='hi', - content='look at this', + name="hi", + content="look at this", user=self.local_user, book=self.book, - privacy='public', + privacy="public", ) - request = self.factory.get('', {'page': 1}, HTTP_USER_AGENT=USER_AGENT) - result = views.Outbox.as_view()(request, 'mouse') + request = self.factory.get("", {"page": 1}, HTTP_USER_AGENT=USER_AGENT) + result = views.Outbox.as_view()(request, "mouse") data = json.loads(result.content) - self.assertEqual(len(data['orderedItems']), 1) - self.assertEqual(data['orderedItems'][0]['type'], 'Review') + self.assertEqual(len(data["orderedItems"]), 1) + self.assertEqual(data["orderedItems"][0]["type"], "Review") def test_outbox_bookwyrm_request_false(self): - ''' should differentiate between bookwyrm and outside requests ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ should differentiate between bookwyrm and outside requests """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.Review.objects.create( - name='hi', - content='look at this', + name="hi", + content="look at this", user=self.local_user, book=self.book, - privacy='public', + privacy="public", ) - request = self.factory.get('', {'page': 1}) - result = views.Outbox.as_view()(request, 'mouse') + request = self.factory.get("", {"page": 1}) + result = views.Outbox.as_view()(request, "mouse") data = json.loads(result.content) - self.assertEqual(len(data['orderedItems']), 1) - self.assertEqual(data['orderedItems'][0]['type'], 'Article') + self.assertEqual(len(data["orderedItems"]), 1) + self.assertEqual(data["orderedItems"][0]["type"], "Article") diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py index 9fc37fdb9..f67f5538f 100644 --- a/bookwyrm/tests/views/test_password.py +++ b/bookwyrm/tests/views/test_password.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.contrib.auth.models import AnonymousUser @@ -10,22 +10,26 @@ from bookwyrm import models, views class PasswordViews(TestCase): - ''' view user and edit profile ''' + """ view user and edit profile """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'password', - local=True, localname='mouse') + "mouse@local.com", + "mouse@mouse.com", + "password", + local=True, + localname="mouse", + ) self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False models.SiteSettings.objects.create(id=1) - def test_password_reset_request(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.PasswordResetRequest.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user result = view(request) @@ -33,76 +37,63 @@ class PasswordViews(TestCase): result.render() self.assertEqual(result.status_code, 200) - def test_password_reset_request_post(self): - ''' send 'em an email ''' - request = self.factory.post('', {'email': 'aa@bb.ccc'}) + """ send 'em an email """ + request = self.factory.post("", {"email": "aa@bb.ccc"}) view = views.PasswordResetRequest.as_view() resp = view(request) self.assertEqual(resp.status_code, 302) - request = self.factory.post('', {'email': 'mouse@mouse.com'}) - with patch('bookwyrm.emailing.send_email.delay'): + request = self.factory.post("", {"email": "mouse@mouse.com"}) + with patch("bookwyrm.emailing.send_email.delay"): resp = view(request) resp.render() - self.assertEqual( - models.PasswordReset.objects.get().user, self.local_user) + self.assertEqual(models.PasswordReset.objects.get().user, self.local_user) def test_password_reset(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) - request = self.factory.get('') + request = self.factory.get("") request.user = self.anonymous_user result = view(request, code.code) self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - def test_password_reset_post(self): - ''' reset from code ''' + """ reset from code """ view = views.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hi' - }) - with patch('bookwyrm.views.password.login'): + request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) + with patch("bookwyrm.views.password.login"): resp = view(request, code.code) self.assertEqual(resp.status_code, 302) self.assertFalse(models.PasswordReset.objects.exists()) def test_password_reset_wrong_code(self): - ''' reset from code ''' + """ reset from code """ view = views.PasswordReset.as_view() models.PasswordReset.objects.create(user=self.local_user) - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hi' - }) - resp = view(request, 'jhgdkfjgdf') + request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) + resp = view(request, "jhgdkfjgdf") resp.render() self.assertTrue(models.PasswordReset.objects.exists()) def test_password_reset_mismatch(self): - ''' reset from code ''' + """ reset from code """ view = views.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hihi' - }) + request = self.factory.post("", {"password": "hi", "confirm-password": "hihi"}) 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 ''' + """ there are so many views, this just makes sure it LOADS """ view = views.ChangePassword.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user result = view(request) @@ -110,28 +101,21 @@ class PasswordViews(TestCase): result.render() self.assertEqual(result.status_code, 200) - def test_password_change(self): - ''' change password ''' + """ change password """ view = views.ChangePassword.as_view() password_hash = self.local_user.password - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hi' - }) + request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) request.user = self.local_user - with patch('bookwyrm.views.password.login'): + with patch("bookwyrm.views.password.login"): view(request) self.assertNotEqual(self.local_user.password, password_hash) def test_password_change_mismatch(self): - ''' change password ''' + """ change password """ view = views.ChangePassword.as_view() password_hash = self.local_user.password - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hihi' - }) + 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 2bb052ae2..96d1f1f4c 100644 --- a/bookwyrm/tests/views/test_reading.py +++ b/bookwyrm/tests/views/test_reading.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch import dateutil from django.test import TestCase @@ -7,45 +7,54 @@ from django.utils import timezone from bookwyrm import models, views + class ReadingViews(TestCase): - ''' viewing and creating statuses ''' + """ viewing and creating statuses """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Test Book', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Test Book", + remote_id="https://example.com/book/1", + parent_work=self.work, ) - with patch('bookwyrm.models.user.set_remote_server.delay'): + with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'ratword', + "rat", + "rat@rat.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) - def test_start_reading(self): - ''' begin a book ''' - shelf = self.local_user.shelf_set.get(identifier='reading') + """ begin a book """ + shelf = self.local_user.shelf_set.get(identifier="reading") self.assertFalse(shelf.books.exists()) self.assertFalse(models.Status.objects.exists()) - request = self.factory.post('', { - 'post-status': True, - 'privacy': 'followers', - 'start_date': '2020-01-05', - }) + request = self.factory.post( + "", + { + "post-status": True, + "privacy": "followers", + "start_date": "2020-01-05", + }, + ) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.start_reading(request, self.book.id) self.assertEqual(shelf.books.get(), self.book) @@ -53,7 +62,7 @@ class ReadingViews(TestCase): status = models.GeneratedNote.objects.get() self.assertEqual(status.user, self.local_user) self.assertEqual(status.mention_books.get(), self.book) - self.assertEqual(status.privacy, 'followers') + self.assertEqual(status.privacy, "followers") readthrough = models.ReadThrough.objects.get() self.assertIsNotNone(readthrough.start_date) @@ -61,45 +70,47 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.user, self.local_user) self.assertEqual(readthrough.book, self.book) - def test_start_reading_reshelf(self): - ''' begin a book ''' - to_read_shelf = self.local_user.shelf_set.get(identifier='to-read') - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ begin a book """ + to_read_shelf = self.local_user.shelf_set.get(identifier="to-read") + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( - shelf=to_read_shelf, book=self.book, user=self.local_user) - shelf = self.local_user.shelf_set.get(identifier='reading') + shelf=to_read_shelf, book=self.book, user=self.local_user + ) + shelf = self.local_user.shelf_set.get(identifier="reading") self.assertEqual(to_read_shelf.books.get(), self.book) self.assertFalse(shelf.books.exists()) self.assertFalse(models.Status.objects.exists()) - request = self.factory.post('') + request = self.factory.post("") request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.start_reading(request, self.book.id) self.assertFalse(to_read_shelf.books.exists()) self.assertEqual(shelf.books.get(), self.book) def test_finish_reading(self): - ''' begin a book ''' - shelf = self.local_user.shelf_set.get(identifier='read') + """ begin a book """ + shelf = self.local_user.shelf_set.get(identifier="read") self.assertFalse(shelf.books.exists()) self.assertFalse(models.Status.objects.exists()) readthrough = models.ReadThrough.objects.create( - user=self.local_user, - start_date=timezone.now(), - book=self.book) + user=self.local_user, start_date=timezone.now(), book=self.book + ) - request = self.factory.post('', { - 'post-status': True, - 'privacy': 'followers', - 'finish_date': '2020-01-07', - 'id': readthrough.id, - }) + request = self.factory.post( + "", + { + "post-status": True, + "privacy": "followers", + "finish_date": "2020-01-07", + "id": readthrough.id, + }, + ) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.finish_reading(request, self.book.id) self.assertEqual(shelf.books.get(), self.book) @@ -107,7 +118,7 @@ class ReadingViews(TestCase): status = models.GeneratedNote.objects.get() self.assertEqual(status.user, self.local_user) self.assertEqual(status.mention_books.get(), self.book) - self.assertEqual(status.privacy, 'followers') + self.assertEqual(status.privacy, "followers") readthrough = models.ReadThrough.objects.get() self.assertIsNotNone(readthrough.start_date) @@ -115,19 +126,21 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.user, self.local_user) self.assertEqual(readthrough.book, self.book) - def test_edit_readthrough(self): - ''' adding dates to an ongoing readthrough ''' - start = timezone.make_aware(dateutil.parser.parse('2021-01-03')) + """ adding dates to an ongoing readthrough """ + start = timezone.make_aware(dateutil.parser.parse("2021-01-03")) readthrough = models.ReadThrough.objects.create( - book=self.book, user=self.local_user, start_date=start) + book=self.book, user=self.local_user, start_date=start + ) request = self.factory.post( - '', { - 'start_date': '2017-01-01', - 'finish_date': '2018-03-07', - 'book': '', - 'id': readthrough.id, - }) + "", + { + "start_date": "2017-01-01", + "finish_date": "2018-03-07", + "book": "", + "id": readthrough.id, + }, + ) request.user = self.local_user views.edit_readthrough(request) @@ -140,33 +153,34 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.finish_date.day, 7) self.assertEqual(readthrough.book, self.book) - def test_delete_readthrough(self): - ''' remove a readthrough ''' + """ remove a readthrough """ readthrough = models.ReadThrough.objects.create( - book=self.book, user=self.local_user) - models.ReadThrough.objects.create( - book=self.book, user=self.local_user) + book=self.book, user=self.local_user + ) + models.ReadThrough.objects.create(book=self.book, user=self.local_user) request = self.factory.post( - '', { - 'id': readthrough.id, - }) + "", + { + "id": readthrough.id, + }, + ) request.user = self.local_user views.delete_readthrough(request) - self.assertFalse( - models.ReadThrough.objects.filter(id=readthrough.id).exists()) - + self.assertFalse(models.ReadThrough.objects.filter(id=readthrough.id).exists()) def test_create_readthrough(self): - ''' adding new read dates ''' + """ adding new read dates """ request = self.factory.post( - '', { - 'start_date': '2017-01-01', - 'finish_date': '2018-03-07', - 'book': self.book.id, - 'id': '', - }) + "", + { + "start_date": "2017-01-01", + "finish_date": "2018-03-07", + "book": self.book.id, + "id": "", + }, + ) request.user = self.local_user views.create_readthrough(request) diff --git a/bookwyrm/tests/views/test_readthrough.py b/bookwyrm/tests/views/test_readthrough.py index 41e38a1d6..5399f673c 100644 --- a/bookwyrm/tests/views/test_readthrough.py +++ b/bookwyrm/tests/views/test_readthrough.py @@ -1,4 +1,4 @@ -''' tests updating reading progress ''' +""" tests updating reading progress """ from datetime import datetime from unittest.mock import patch from django.test import TestCase, Client @@ -6,63 +6,68 @@ from django.utils import timezone from bookwyrm import models -@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') + +@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class ReadThrough(TestCase): - ''' readthrough tests ''' + """ readthrough tests """ + def setUp(self): - ''' basic user and book data ''' + """ basic user and book data """ self.client = Client() - self.work = models.Work.objects.create( - title='Example Work' - ) + self.work = models.Work.objects.create(title="Example Work") self.edition = models.Edition.objects.create( - title='Example Edition', - parent_work=self.work + title="Example Edition", parent_work=self.work ) self.work.default_edition = self.edition self.work.save() self.user = models.User.objects.create_user( - 'cinco', 'cinco@example.com', 'seissiete', - local=True, localname='cinco') + "cinco", "cinco@example.com", "seissiete", local=True, localname="cinco" + ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): self.client.force_login(self.user) def test_create_basic_readthrough(self, delay_mock): """A basic readthrough doesn't create a progress update""" self.assertEqual(self.edition.readthrough_set.count(), 0) - self.client.post('/start-reading/{}'.format(self.edition.id), { - 'start_date': '2020-11-27', - }) + self.client.post( + "/start-reading/{}".format(self.edition.id), + { + "start_date": "2020-11-27", + }, + ) readthroughs = self.edition.readthrough_set.all() self.assertEqual(len(readthroughs), 1) self.assertEqual(readthroughs[0].progressupdate_set.count(), 0) self.assertEqual( - readthroughs[0].start_date, - datetime(2020, 11, 27, tzinfo=timezone.utc)) + readthroughs[0].start_date, datetime(2020, 11, 27, tzinfo=timezone.utc) + ) self.assertEqual(readthroughs[0].progress, None) self.assertEqual(readthroughs[0].finish_date, None) self.assertEqual(delay_mock.call_count, 1) def test_create_progress_readthrough(self, delay_mock): - ''' a readthrough with progress ''' + """ a readthrough with progress """ self.assertEqual(self.edition.readthrough_set.count(), 0) - self.client.post('/start-reading/{}'.format(self.edition.id), { - 'start_date': '2020-11-27', - 'progress': 50, - }) + self.client.post( + "/start-reading/{}".format(self.edition.id), + { + "start_date": "2020-11-27", + "progress": 50, + }, + ) readthroughs = self.edition.readthrough_set.all() self.assertEqual(len(readthroughs), 1) self.assertEqual( - readthroughs[0].start_date, - datetime(2020, 11, 27, tzinfo=timezone.utc)) + readthroughs[0].start_date, datetime(2020, 11, 27, tzinfo=timezone.utc) + ) self.assertEqual(readthroughs[0].progress, 50) self.assertEqual(readthroughs[0].finish_date, None) @@ -73,13 +78,17 @@ class ReadThrough(TestCase): self.assertEqual(delay_mock.call_count, 1) # Update progress - self.client.post('/edit-readthrough', { - 'id': readthroughs[0].id, - 'progress': 100, - }) + self.client.post( + "/edit-readthrough", + { + "id": readthroughs[0].id, + "progress": 100, + }, + ) - progress_updates = readthroughs[0].progressupdate_set\ - .order_by('updated_date').all() + progress_updates = ( + readthroughs[0].progressupdate_set.order_by("updated_date").all() + ) self.assertEqual(len(progress_updates), 2) self.assertEqual(progress_updates[1].mode, models.ProgressMode.PAGE) self.assertEqual(progress_updates[1].progress, 100) @@ -87,9 +96,12 @@ class ReadThrough(TestCase): # Edit doesn't publish anything self.assertEqual(delay_mock.call_count, 1) - self.client.post('/delete-readthrough', { - 'id': readthroughs[0].id, - }) + self.client.post( + "/delete-readthrough", + { + "id": readthroughs[0].id, + }, + ) readthroughs = self.edition.readthrough_set.all() updates = self.user.progressupdate_set.all() diff --git a/bookwyrm/tests/views/test_rss_feed.py b/bookwyrm/tests/views/test_rss_feed.py index 3d5bec49a..c7fef2a94 100644 --- a/bookwyrm/tests/views/test_rss_feed.py +++ b/bookwyrm/tests/views/test_rss_feed.py @@ -1,4 +1,4 @@ -''' testing import ''' +""" testing import """ from unittest.mock import patch from django.test import RequestFactory, TestCase @@ -6,41 +6,51 @@ from django.test import RequestFactory, TestCase from bookwyrm import models from bookwyrm.views import rss_feed + class RssFeedView(TestCase): - ''' rss feed behaves as expected ''' + """ rss feed behaves as expected """ + def setUp(self): - ''' test data ''' + """ test data """ self.site = models.SiteSettings.objects.create() self.user = models.User.objects.create_user( - 'rss_user', 'rss@test.rss', 'password', local=True) - - work = models.Work.objects.create(title='Test Work') - self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=work + "rss_user", "rss@test.rss", "password", local=True ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + work = models.Work.objects.create(title="Test Work") + self.book = models.Edition.objects.create( + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=work, + ) + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): self.review = models.Review.objects.create( - name='Review name', content='test content', rating=3, - user=self.user, book=self.book) + name="Review name", + content="test content", + rating=3, + user=self.user, + book=self.book, + ) self.quote = models.Quotation.objects.create( - quote='a sickening sense', content='test content', - user=self.user, book=self.book) + quote="a sickening sense", + content="test content", + user=self.user, + book=self.book, + ) self.generatednote = models.GeneratedNote.objects.create( - content='test content', user=self.user) + content="test content", user=self.user + ) self.factory = RequestFactory() - def test_rss_feed(self): - ''' load an rss feed ''' + """ load an rss feed """ view = rss_feed.RssFeed() - request = self.factory.get('/user/rss_user/rss') + request = self.factory.get("/user/rss_user/rss") request.user = self.user with patch("bookwyrm.models.SiteSettings.objects.get") as site: site.return_value = self.site diff --git a/bookwyrm/tests/views/test_search.py b/bookwyrm/tests/views/test_search.py index 5d7109e71..78c7a1037 100644 --- a/bookwyrm/tests/views/test_search.py +++ b/bookwyrm/tests/views/test_search.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ import json from unittest.mock import patch @@ -13,103 +13,107 @@ from bookwyrm.settings import DOMAIN class ShelfViews(TestCase): - ''' tag views''' + """ tag views""" + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Test Book', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Test Book", + remote_id="https://example.com/book/1", + parent_work=self.work, ) models.Connector.objects.create( - identifier='self', - connector_file='self_connector', - local=True + identifier="self", connector_file="self_connector", local=True ) models.SiteSettings.objects.create() - def test_search_json_response(self): - ''' searches local data only and returns book data in json format ''' + """ searches local data only and returns book data in json format """ view = views.Search.as_view() # we need a connector for this, sorry - request = self.factory.get('', {'q': 'Test Book'}) - with patch('bookwyrm.views.search.is_api_request') as is_api: + request = self.factory.get("", {"q": "Test Book"}) + with patch("bookwyrm.views.search.is_api_request") as is_api: is_api.return_value = True response = view(request) self.assertIsInstance(response, JsonResponse) data = json.loads(response.content) self.assertEqual(len(data), 1) - self.assertEqual(data[0]['title'], 'Test Book') - self.assertEqual( - data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id)) - + self.assertEqual(data[0]["title"], "Test Book") + self.assertEqual(data[0]["key"], "https://%s/book/%d" % (DOMAIN, self.book.id)) def test_search_html_response(self): - ''' searches remote connectors ''' + """ searches remote connectors """ view = views.Search.as_view() + class TestConnector(abstract_connector.AbstractMinimalConnector): - ''' nothing added here ''' + """ nothing added here """ + def format_search_result(self, search_result): pass + def get_or_create_book(self, remote_id): pass + def parse_search_data(self, data): pass + def format_isbn_search_result(self, search_result): return search_result + def parse_isbn_search_data(self, data): return data + models.Connector.objects.create( - identifier='example.com', - connector_file='openlibrary', - base_url='https://example.com', - books_url='https://example.com/books', - covers_url='https://example.com/covers', - search_url='https://example.com/search?q=', + identifier="example.com", + connector_file="openlibrary", + base_url="https://example.com", + books_url="https://example.com/books", + covers_url="https://example.com/covers", + search_url="https://example.com/search?q=", ) - connector = TestConnector('example.com') + connector = TestConnector("example.com") search_result = abstract_connector.SearchResult( - key='http://www.example.com/book/1', - title='Gideon the Ninth', - author='Tamsyn Muir', - year='2019', - connector=connector + key="http://www.example.com/book/1", + title="Gideon the Ninth", + author="Tamsyn Muir", + year="2019", + connector=connector, ) - request = self.factory.get('', {'q': 'Test Book'}) + request = self.factory.get("", {"q": "Test Book"}) request.user = self.local_user - with patch('bookwyrm.views.search.is_api_request') as is_api: + with patch("bookwyrm.views.search.is_api_request") as is_api: is_api.return_value = False - with patch( - 'bookwyrm.connectors.connector_manager.search') as manager: + with patch("bookwyrm.connectors.connector_manager.search") as manager: manager.return_value = [search_result] response = view(request) self.assertIsInstance(response, TemplateResponse) response.render() self.assertEqual( - response.context_data['book_results'][0].title, 'Gideon the Ninth') - + response.context_data["book_results"][0].title, "Gideon the Ninth" + ) def test_search_html_response_users(self): - ''' searches remote connectors ''' + """ searches remote connectors """ view = views.Search.as_view() - request = self.factory.get('', {'q': 'mouse'}) + request = self.factory.get("", {"q": "mouse"}) request.user = self.local_user - with patch('bookwyrm.views.search.is_api_request') as is_api: + with patch("bookwyrm.views.search.is_api_request") as is_api: is_api.return_value = False - with patch('bookwyrm.connectors.connector_manager.search'): + with patch("bookwyrm.connectors.connector_manager.search"): response = view(request) self.assertIsInstance(response, TemplateResponse) response.render() - self.assertEqual( - response.context_data['user_results'][0], self.local_user) + self.assertEqual(response.context_data["user_results"][0], self.local_user) diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index 4fa63689a..a308fe56a 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase @@ -8,195 +8,172 @@ from bookwyrm import models, views from bookwyrm.activitypub import ActivitypubResponse -@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') +@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class ShelfViews(TestCase): - ''' tag views''' + """ tag views""" + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=self.work, ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): self.shelf = models.Shelf.objects.create( - name='Test Shelf', - identifier='test-shelf', - user=self.local_user + name="Test Shelf", identifier="test-shelf", user=self.local_user ) models.SiteSettings.objects.create() - def test_shelf_page(self, _): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Shelf.as_view() shelf = self.local_user.shelf_set.first() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.views.shelf.is_api_request') as is_api: + with patch("bookwyrm.views.shelf.is_api_request") as is_api: is_api.return_value = False result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - with patch('bookwyrm.views.shelf.is_api_request') as is_api: + with patch("bookwyrm.views.shelf.is_api_request") as is_api: is_api.return_value = True - result = view( - request, self.local_user.username, shelf.identifier) + result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - request = self.factory.get('/?page=1') + request = self.factory.get("/?page=1") request.user = self.local_user - with patch('bookwyrm.views.shelf.is_api_request') as is_api: + with patch("bookwyrm.views.shelf.is_api_request") as is_api: is_api.return_value = True - result = view( - request, self.local_user.username, shelf.identifier) + result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_edit_shelf_privacy(self, _): - ''' set name or privacy on shelf ''' + """ set name or privacy on shelf """ view = views.Shelf.as_view() - shelf = self.local_user.shelf_set.get(identifier='to-read') - self.assertEqual(shelf.privacy, 'public') + shelf = self.local_user.shelf_set.get(identifier="to-read") + self.assertEqual(shelf.privacy, "public") request = self.factory.post( - '', { - 'privacy': 'unlisted', - 'user': self.local_user.id, - 'name': 'To Read', - }) + "", + { + "privacy": "unlisted", + "user": self.local_user.id, + "name": "To Read", + }, + ) request.user = self.local_user view(request, self.local_user.username, shelf.identifier) shelf.refresh_from_db() - self.assertEqual(shelf.privacy, 'unlisted') - + self.assertEqual(shelf.privacy, "unlisted") def test_edit_shelf_name(self, _): - ''' change the name of an editable shelf ''' + """ change the name of an editable shelf """ view = views.Shelf.as_view() - shelf = models.Shelf.objects.create( - name='Test Shelf', user=self.local_user) - self.assertEqual(shelf.privacy, 'public') + shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user) + self.assertEqual(shelf.privacy, "public") request = self.factory.post( - '', { - 'privacy': 'public', - 'user': self.local_user.id, - 'name': 'cool name' - }) + "", {"privacy": "public", "user": self.local_user.id, "name": "cool name"} + ) 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, request.user.username, shelf.identifier) shelf.refresh_from_db() - self.assertEqual(shelf.name, 'cool name') - self.assertEqual(shelf.identifier, 'testshelf-%d' % shelf.id) - + self.assertEqual(shelf.name, "cool name") + self.assertEqual(shelf.identifier, "testshelf-%d" % shelf.id) def test_edit_shelf_name_not_editable(self, _): - ''' can't change the name of an non-editable shelf ''' + """ can't change the name of an non-editable shelf """ view = views.Shelf.as_view() - shelf = self.local_user.shelf_set.get(identifier='to-read') - self.assertEqual(shelf.privacy, 'public') + shelf = self.local_user.shelf_set.get(identifier="to-read") + self.assertEqual(shelf.privacy, "public") request = self.factory.post( - '', { - 'privacy': 'public', - 'user': self.local_user.id, - 'name': 'cool name' - }) + "", {"privacy": "public", "user": self.local_user.id, "name": "cool name"} + ) 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, request.user.username, shelf.identifier) - self.assertEqual(shelf.name, 'To Read') - + self.assertEqual(shelf.name, "To Read") def test_handle_shelve(self, _): - ''' shelve a book ''' - request = self.factory.post('', { - 'book': self.book.id, - 'shelf': self.shelf.identifier - }) + """ shelve a book """ + request = self.factory.post( + "", {"book": self.book.id, "shelf": self.shelf.identifier} + ) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.shelve(request) # make sure the book is on the shelf self.assertEqual(self.shelf.books.get(), self.book) - def test_handle_shelve_to_read(self, _): - ''' special behavior for the to-read shelf ''' - shelf = models.Shelf.objects.get(identifier='to-read') - request = self.factory.post('', { - 'book': self.book.id, - 'shelf': shelf.identifier - }) + """ special behavior for the to-read shelf """ + shelf = models.Shelf.objects.get(identifier="to-read") + request = self.factory.post( + "", {"book": self.book.id, "shelf": shelf.identifier} + ) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.shelve(request) # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - def test_handle_shelve_reading(self, _): - ''' special behavior for the reading shelf ''' - shelf = models.Shelf.objects.get(identifier='reading') - request = self.factory.post('', { - 'book': self.book.id, - 'shelf': shelf.identifier - }) + """ special behavior for the reading shelf """ + shelf = models.Shelf.objects.get(identifier="reading") + request = self.factory.post( + "", {"book": self.book.id, "shelf": shelf.identifier} + ) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.shelve(request) # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - def test_handle_shelve_read(self, _): - ''' special behavior for the read shelf ''' - shelf = models.Shelf.objects.get(identifier='read') - request = self.factory.post('', { - 'book': self.book.id, - 'shelf': shelf.identifier - }) + """ special behavior for the read shelf """ + shelf = models.Shelf.objects.get(identifier="read") + request = self.factory.post( + "", {"book": self.book.id, "shelf": shelf.identifier} + ) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.shelve(request) # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - def test_handle_unshelve(self, _): - ''' remove a book from a shelf ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + """ remove a book from a shelf """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( - book=self.book, - user=self.local_user, - shelf=self.shelf + book=self.book, user=self.local_user, shelf=self.shelf ) self.shelf.save() self.assertEqual(self.shelf.books.count(), 1) - request = self.factory.post('', { - 'book': self.book.id, - 'shelf': self.shelf.id - }) + request = self.factory.post("", {"book": self.book.id, "shelf": self.shelf.id}) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.unshelve(request) self.assertEqual(self.shelf.books.count(), 0) diff --git a/bookwyrm/tests/views/test_status.py b/bookwyrm/tests/views/test_status.py index 4594a2031..7fdc0e060 100644 --- a/bookwyrm/tests/views/test_status.py +++ b/bookwyrm/tests/views/test_status.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ import json from unittest.mock import patch from django.test import TestCase @@ -9,239 +9,247 @@ from bookwyrm.settings import DOMAIN class StatusViews(TestCase): - ''' viewing and creating statuses ''' + """ viewing and creating statuses """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - with patch('bookwyrm.models.user.set_remote_server'): + with patch("bookwyrm.models.user.set_remote_server"): self.remote_user = models.User.objects.create_user( - 'rat', 'rat@email.com', 'ratword', + "rat", + "rat@email.com", + "ratword", local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", ) - work = models.Work.objects.create(title='Test Work') + work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=work, ) - def test_handle_status(self): - ''' create a status ''' + """ create a status """ view = views.CreateStatus.as_view() - form = forms.CommentForm({ - 'content': 'hi', - 'user': self.local_user.id, - 'book': self.book.id, - 'privacy': 'public', - }) - request = self.factory.post('', form.data) + form = forms.CommentForm( + { + "content": "hi", + "user": self.local_user.id, + "book": self.book.id, + "privacy": "public", + } + ) + request = self.factory.post("", form.data) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - view(request, 'comment') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request, "comment") status = models.Comment.objects.get() - self.assertEqual(status.content, '

hi

') + self.assertEqual(status.content, "

hi

") self.assertEqual(status.user, self.local_user) self.assertEqual(status.book, self.book) def test_handle_status_reply(self): - ''' create a status in reply to an existing status ''' + """ create a status in reply to an existing status """ view = views.CreateStatus.as_view() user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'password', local=True) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + "rat", "rat@rat.com", "password", local=True + ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): parent = models.Status.objects.create( - content='parent status', user=self.local_user) - form = forms.ReplyForm({ - 'content': 'hi', - 'user': user.id, - 'reply_parent': parent.id, - 'privacy': 'public', - }) - request = self.factory.post('', form.data) + content="parent status", user=self.local_user + ) + form = forms.ReplyForm( + { + "content": "hi", + "user": user.id, + "reply_parent": parent.id, + "privacy": "public", + } + ) + request = self.factory.post("", form.data) request.user = user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - view(request, 'reply') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request, "reply") status = models.Status.objects.get(user=user) - self.assertEqual(status.content, '

hi

') + self.assertEqual(status.content, "

hi

") self.assertEqual(status.user, user) - self.assertEqual( - models.Notification.objects.get().user, self.local_user) + self.assertEqual(models.Notification.objects.get().user, self.local_user) def test_handle_status_mentions(self): - ''' @mention a user in a post ''' + """ @mention a user in a post """ view = views.CreateStatus.as_view() user = models.User.objects.create_user( - 'rat@%s' % DOMAIN, 'rat@rat.com', 'password', - local=True, localname='rat') - form = forms.CommentForm({ - 'content': 'hi @rat', - 'user': self.local_user.id, - 'book': self.book.id, - 'privacy': 'public', - }) - request = self.factory.post('', form.data) + "rat@%s" % DOMAIN, "rat@rat.com", "password", local=True, localname="rat" + ) + form = forms.CommentForm( + { + "content": "hi @rat", + "user": self.local_user.id, + "book": self.book.id, + "privacy": "public", + } + ) + request = self.factory.post("", form.data) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - view(request, 'comment') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request, "comment") status = models.Status.objects.get() self.assertEqual(list(status.mention_users.all()), [user]) self.assertEqual(models.Notification.objects.get().user, user) self.assertEqual( - status.content, - '

hi @rat

' % user.remote_id) + status.content, '

hi @rat

' % user.remote_id + ) def test_handle_status_reply_with_mentions(self): - ''' reply to a post with an @mention'ed user ''' + """ reply to a post with an @mention'ed user """ view = views.CreateStatus.as_view() user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'password', - local=True, localname='rat') - form = forms.CommentForm({ - 'content': 'hi @rat@example.com', - 'user': self.local_user.id, - 'book': self.book.id, - 'privacy': 'public', - }) - request = self.factory.post('', form.data) + "rat", "rat@rat.com", "password", local=True, localname="rat" + ) + form = forms.CommentForm( + { + "content": "hi @rat@example.com", + "user": self.local_user.id, + "book": self.book.id, + "privacy": "public", + } + ) + request = self.factory.post("", form.data) request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - view(request, 'comment') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request, "comment") status = models.Status.objects.get() - form = forms.ReplyForm({ - 'content': 'right', - 'user': user.id, - 'privacy': 'public', - 'reply_parent': status.id - }) - request = self.factory.post('', form.data) + form = forms.ReplyForm( + { + "content": "right", + "user": user.id, + "privacy": "public", + "reply_parent": status.id, + } + ) + request = self.factory.post("", form.data) request.user = user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - view(request, 'reply') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request, "reply") reply = models.Status.replies(status).first() - self.assertEqual(reply.content, '

right

') + self.assertEqual(reply.content, "

right

") self.assertEqual(reply.user, user) # the mentioned user in the parent post is only included if @'ed self.assertFalse(self.remote_user in reply.mention_users.all()) self.assertTrue(self.local_user in reply.mention_users.all()) def test_find_mentions(self): - ''' detect and look up @ mentions of users ''' + """ detect and look up @ mentions of users """ user = models.User.objects.create_user( - 'nutria@%s' % DOMAIN, 'nutria@nutria.com', 'password', - local=True, localname='nutria') - self.assertEqual(user.username, 'nutria@%s' % DOMAIN) + "nutria@%s" % DOMAIN, + "nutria@nutria.com", + "password", + local=True, + localname="nutria", + ) + self.assertEqual(user.username, "nutria@%s" % DOMAIN) self.assertEqual( - list(views.status.find_mentions('@nutria'))[0], - ('@nutria', user) + list(views.status.find_mentions("@nutria"))[0], ("@nutria", user) ) self.assertEqual( - list(views.status.find_mentions('leading text @nutria'))[0], - ('@nutria', user) + list(views.status.find_mentions("leading text @nutria"))[0], + ("@nutria", user), ) self.assertEqual( - list(views.status.find_mentions( - 'leading @nutria trailing text'))[0], - ('@nutria', user) + list(views.status.find_mentions("leading @nutria trailing text"))[0], + ("@nutria", user), ) self.assertEqual( - list(views.status.find_mentions( - '@rat@example.com'))[0], - ('@rat@example.com', self.remote_user) + list(views.status.find_mentions("@rat@example.com"))[0], + ("@rat@example.com", self.remote_user), ) - multiple = list(views.status.find_mentions( - '@nutria and @rat@example.com')) - self.assertEqual(multiple[0], ('@nutria', user)) - self.assertEqual(multiple[1], ('@rat@example.com', self.remote_user)) + multiple = list(views.status.find_mentions("@nutria and @rat@example.com")) + self.assertEqual(multiple[0], ("@nutria", user)) + self.assertEqual(multiple[1], ("@rat@example.com", self.remote_user)) - with patch('bookwyrm.views.status.handle_remote_webfinger') as rw: + with patch("bookwyrm.views.status.handle_remote_webfinger") as rw: rw.return_value = self.local_user self.assertEqual( - list(views.status.find_mentions('@beep@beep.com'))[0], - ('@beep@beep.com', self.local_user) + list(views.status.find_mentions("@beep@beep.com"))[0], + ("@beep@beep.com", self.local_user), ) - with patch('bookwyrm.views.status.handle_remote_webfinger') as rw: + with patch("bookwyrm.views.status.handle_remote_webfinger") as rw: rw.return_value = None - self.assertEqual(list(views.status.find_mentions( - '@beep@beep.com')), []) + self.assertEqual(list(views.status.find_mentions("@beep@beep.com")), []) self.assertEqual( - list(views.status.find_mentions('@nutria@%s' % DOMAIN))[0], - ('@nutria@%s' % DOMAIN, user) + list(views.status.find_mentions("@nutria@%s" % DOMAIN))[0], + ("@nutria@%s" % DOMAIN, user), ) def test_format_links(self): - ''' find and format urls into a tags ''' - url = 'http://www.fish.com/' + """ find and format urls into a tags """ + url = "http://www.fish.com/" + self.assertEqual( + views.status.format_links(url), 'www.fish.com/' % url + ) + self.assertEqual( + views.status.format_links("(%s)" % url), + '(www.fish.com/)' % url, + ) + url = "https://archive.org/details/dli.granth.72113/page/n25/mode/2up" self.assertEqual( views.status.format_links(url), - 'www.fish.com/' % url) - self.assertEqual( - views.status.format_links('(%s)' % url), - '(www.fish.com/)' % url) - url = 'https://archive.org/details/dli.granth.72113/page/n25/mode/2up' + '' + "archive.org/details/dli.granth.72113/page/n25/mode/2up" % url, + ) + url = "https://openlibrary.org/search" "?q=arkady+strugatsky&mode=everything" self.assertEqual( views.status.format_links(url), - '' \ - 'archive.org/details/dli.granth.72113/page/n25/mode/2up' \ - % url) - url = 'https://openlibrary.org/search' \ - '?q=arkady+strugatsky&mode=everything' - self.assertEqual( - views.status.format_links(url), - 'openlibrary.org/search' \ - '?q=arkady+strugatsky&mode=everything' % url) - + 'openlibrary.org/search' + "?q=arkady+strugatsky&mode=everything" % url, + ) def test_to_markdown(self): - ''' this is mostly handled in other places, but nonetheless ''' - text = '_hi_ and http://fish.com is rad' + """ this is mostly handled in other places, but nonetheless """ + text = "_hi_ and http://fish.com is rad" result = views.status.to_markdown(text) self.assertEqual( result, - '

hi and fish.com ' \ - 'is rad

') - + '

hi and fish.com ' "is rad

", + ) def test_to_markdown_link(self): - ''' this is mostly handled in other places, but nonetheless ''' - text = '[hi](http://fish.com) is rad' + """ this is mostly handled in other places, but nonetheless """ + text = "[hi](http://fish.com) is rad" result = views.status.to_markdown(text) - self.assertEqual( - result, - '

hi ' \ - 'is rad

') - + self.assertEqual(result, '

hi ' "is rad

") def test_handle_delete_status(self): - ''' marks a status as deleted ''' + """ marks a status as deleted """ view = views.DeleteStatus.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - status = models.Status.objects.create( - user=self.local_user, content='hi') + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + status = models.Status.objects.create(user=self.local_user, content="hi") self.assertFalse(status.deleted) - request = self.factory.post('') + request = self.factory.post("") request.user = self.local_user - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ - as mock: + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: view(request, status.id) activity = json.loads(mock.call_args_list[0][0][1]) - self.assertEqual(activity['type'], 'Delete') - self.assertEqual(activity['object']['type'], 'Tombstone') + self.assertEqual(activity["type"], "Delete") + self.assertEqual(activity["object"]["type"], "Tombstone") status.refresh_from_db() self.assertTrue(status.deleted) diff --git a/bookwyrm/tests/views/test_tag.py b/bookwyrm/tests/views/test_tag.py index ef809b466..6ad6ab254 100644 --- a/bookwyrm/tests/views/test_tag.py +++ b/bookwyrm/tests/views/test_tag.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ from unittest.mock import patch from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType @@ -11,107 +11,109 @@ from bookwyrm.activitypub import ActivitypubResponse class TagViews(TestCase): - ''' tag views''' + """ tag views""" + def setUp(self): - ''' we need basic test data and mocks ''' + """ we need basic test data and mocks """ self.factory = RequestFactory() self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse', - remote_id='https://example.com/users/mouse', + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", ) - self.group = Group.objects.create(name='editor') + self.group = Group.objects.create(name="editor") self.group.permissions.add( Permission.objects.create( - name='edit_book', - codename='edit_book', - content_type=ContentType.objects.get_for_model(models.User)).id + name="edit_book", + codename="edit_book", + content_type=ContentType.objects.get_for_model(models.User), + ).id ) - self.work = models.Work.objects.create(title='Test Work') + self.work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - parent_work=self.work + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=self.work, ) models.SiteSettings.objects.create() - def test_tag_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Tag.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - tag = models.Tag.objects.create(name='hi there') - models.UserTag.objects.create( - tag=tag, user=self.local_user, book=self.book) - request = self.factory.get('') - with patch('bookwyrm.views.tag.is_api_request') as is_api: + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + tag = models.Tag.objects.create(name="hi there") + models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book) + request = self.factory.get("") + with patch("bookwyrm.views.tag.is_api_request") as is_api: is_api.return_value = False result = view(request, tag.identifier) self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - request = self.factory.get('') - with patch('bookwyrm.views.tag.is_api_request') as is_api: + request = self.factory.get("") + with patch("bookwyrm.views.tag.is_api_request") as is_api: is_api.return_value = True result = view(request, tag.identifier) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_tag_page_activitypub_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Tag.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - tag = models.Tag.objects.create(name='hi there') - models.UserTag.objects.create( - tag=tag, user=self.local_user, book=self.book) - request = self.factory.get('', {'page': 1}) - with patch('bookwyrm.views.tag.is_api_request') as is_api: + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + tag = models.Tag.objects.create(name="hi there") + models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book) + request = self.factory.get("", {"page": 1}) + with patch("bookwyrm.views.tag.is_api_request") as is_api: is_api.return_value = True result = view(request, tag.identifier) self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_tag(self): - ''' add a tag to a book ''' + """ add a tag to a book """ view = views.AddTag.as_view() request = self.factory.post( - '', { - 'name': 'A Tag!?', - 'book': self.book.id, - }) + "", + { + "name": "A Tag!?", + "book": self.book.id, + }, + ) 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) tag = models.Tag.objects.get() user_tag = models.UserTag.objects.get() - self.assertEqual(tag.name, 'A Tag!?') - self.assertEqual(tag.identifier, 'A+Tag%21%3F') + self.assertEqual(tag.name, "A Tag!?") + self.assertEqual(tag.identifier, "A+Tag%21%3F") self.assertEqual(user_tag.user, self.local_user) self.assertEqual(user_tag.book, self.book) - def test_untag(self): - ''' remove a tag from a book ''' + """ remove a tag from a book """ view = views.RemoveTag.as_view() - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - tag = models.Tag.objects.create(name='A Tag!?') - models.UserTag.objects.create( - user=self.local_user, book=self.book, tag=tag) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + tag = models.Tag.objects.create(name="A Tag!?") + models.UserTag.objects.create(user=self.local_user, book=self.book, tag=tag) request = self.factory.post( - '', { - 'user': self.local_user.id, - 'book': self.book.id, - 'name': tag.name, - }) + "", + { + "user": self.local_user.id, + "book": self.book.id, + "name": tag.name, + }, + ) 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.assertTrue(models.Tag.objects.filter(name='A Tag!?').exists()) + self.assertTrue(models.Tag.objects.filter(name="A Tag!?").exists()) self.assertFalse(models.UserTag.objects.exists()) diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index f08c17e71..596ea8bf7 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -1,4 +1,4 @@ -''' test for app action functionality ''' +""" test for app action functionality """ import pathlib from unittest.mock import patch from PIL import Image @@ -15,180 +15,177 @@ from bookwyrm.activitypub import ActivitypubResponse class UserViews(TestCase): - ''' view user and edit profile ''' + """ view user and edit profile """ + def setUp(self): - ''' we need basic test data and mocks ''' + """ 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') + "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') + "rat@local.com", "rat@rat.rat", "password", local=True, localname="rat" + ) models.SiteSettings.objects.create() self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False - def test_user_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.User.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'mouse') + result = view(request, "mouse") self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) request.user = self.anonymous_user - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'mouse') + result = view(request, "mouse") self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = True - result = view(request, 'mouse') + result = view(request, "mouse") self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_user_page_blocked(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.User.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user self.rat.blocks.add(self.local_user) - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'rat') + result = view(request, "rat") self.assertEqual(result.status_code, 404) - def test_followers_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Followers.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'mouse') + result = view(request, "mouse") self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = True - result = view(request, 'mouse') + result = view(request, "mouse") self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_followers_page_blocked(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Followers.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user self.rat.blocks.add(self.local_user) - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'rat') + result = view(request, "rat") self.assertEqual(result.status_code, 404) - def test_following_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Following.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'mouse') + result = view(request, "mouse") self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = True - result = view(request, 'mouse') + result = view(request, "mouse") self.assertIsInstance(result, ActivitypubResponse) self.assertEqual(result.status_code, 200) - def test_following_page_blocked(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.Following.as_view() - request = self.factory.get('') + request = self.factory.get("") request.user = self.local_user self.rat.blocks.add(self.local_user) - with patch('bookwyrm.views.user.is_api_request') as is_api: + with patch("bookwyrm.views.user.is_api_request") as is_api: is_api.return_value = False - result = view(request, 'rat') + result = view(request, "rat") self.assertEqual(result.status_code, 404) - def test_edit_user_page(self): - ''' there are so many views, this just makes sure it LOADS ''' + """ there are so many views, this just makes sure it LOADS """ view = views.EditUser.as_view() - request = self.factory.get('') + 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_edit_user(self): - ''' use a form to update a user ''' + """ use a form to update a user """ view = views.EditUser.as_view() form = forms.EditUserForm(instance=self.local_user) - form.data['name'] = 'New Name' - form.data['email'] = 'wow@email.com' - request = self.factory.post('', form.data) + form.data["name"] = "New Name" + form.data["email"] = "wow@email.com" + request = self.factory.post("", form.data) request.user = self.local_user self.assertIsNone(self.local_user.name) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ - as delay_mock: + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as delay_mock: view(request) self.assertEqual(delay_mock.call_count, 1) - self.assertEqual(self.local_user.name, 'New Name') - self.assertEqual(self.local_user.email, 'wow@email.com') + self.assertEqual(self.local_user.name, "New Name") + self.assertEqual(self.local_user.email, "wow@email.com") + # idk how to mock the upload form, got tired of triyng to make it work + # def test_edit_user_avatar(self): + # ''' use a form to update a user ''' + # view = views.EditUser.as_view() + # form = forms.EditUserForm(instance=self.local_user) + # form.data['name'] = 'New Name' + # form.data['email'] = 'wow@email.com' + # image_file = pathlib.Path(__file__).parent.joinpath( + # '../../static/images/no_cover.jpg') + # image = Image.open(image_file) + # form.files['avatar'] = SimpleUploadedFile( + # image_file, open(image_file), content_type='image/jpeg') + # request = self.factory.post('', form.data, form.files) + # request.user = self.local_user -# idk how to mock the upload form, got tired of triyng to make it work -# def test_edit_user_avatar(self): -# ''' use a form to update a user ''' -# view = views.EditUser.as_view() -# form = forms.EditUserForm(instance=self.local_user) -# form.data['name'] = 'New Name' -# form.data['email'] = 'wow@email.com' -# image_file = pathlib.Path(__file__).parent.joinpath( -# '../../static/images/no_cover.jpg') -# image = Image.open(image_file) -# form.files['avatar'] = SimpleUploadedFile( -# image_file, open(image_file), content_type='image/jpeg') -# request = self.factory.post('', form.data, form.files) -# request.user = self.local_user - -# with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ -# as delay_mock: -# view(request) -# self.assertEqual(delay_mock.call_count, 1) -# self.assertEqual(self.local_user.name, 'New Name') -# self.assertEqual(self.local_user.email, 'wow@email.com') -# self.assertIsNotNone(self.local_user.avatar) -# self.assertEqual(self.local_user.avatar.size, (120, 120)) - + # with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ + # as delay_mock: + # view(request) + # self.assertEqual(delay_mock.call_count, 1) + # self.assertEqual(self.local_user.name, 'New Name') + # self.assertEqual(self.local_user.email, 'wow@email.com') + # self.assertIsNotNone(self.local_user.avatar) + # self.assertEqual(self.local_user.avatar.size, (120, 120)) def test_crop_avatar(self): - ''' reduce that image size ''' + """ 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.user.crop_avatar(image) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 1c3da3016..8aaf29516 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -1,4 +1,4 @@ -''' url routing for the app and api ''' +""" url routing for the app and api """ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, re_path @@ -7,170 +7,157 @@ from django.urls import path, re_path from bookwyrm import settings, views, wellknown from bookwyrm.utils import regex -user_path = r'^user/(?P%s)' % regex.username -local_user_path = r'^user/(?P%s)' % regex.localname +user_path = r"^user/(?P%s)" % regex.username +local_user_path = r"^user/(?P%s)" % regex.localname -status_types = [ - 'status', - 'review', - 'comment', - 'quotation', - 'boost', - 'generatednote' -] -status_path = r'%s/(%s)/(?P\d+)' % \ - (user_path, '|'.join(status_types)) +status_types = ["status", "review", "comment", "quotation", "boost", "generatednote"] +status_path = r"%s/(%s)/(?P\d+)" % (user_path, "|".join(status_types)) -book_path = r'^book/(?P\d+)' +book_path = r"^book/(?P\d+)" -handler404 = 'bookwyrm.views.not_found_page' -handler500 = 'bookwyrm.views.server_error_page' +handler404 = "bookwyrm.views.not_found_page" +handler500 = "bookwyrm.views.server_error_page" urlpatterns = [ - path('admin/', admin.site.urls), - + path("admin/", admin.site.urls), # federation endpoints - re_path(r'^inbox/?$', views.Inbox.as_view()), - re_path(r'%s/inbox/?$' % local_user_path, views.Inbox.as_view()), - re_path(r'%s/outbox/?$' % local_user_path, views.Outbox.as_view()), - re_path(r'^.well-known/webfinger/?$', wellknown.webfinger), - re_path(r'^.well-known/nodeinfo/?$', wellknown.nodeinfo_pointer), - re_path(r'^nodeinfo/2\.0/?$', wellknown.nodeinfo), - re_path(r'^api/v1/instance/?$', wellknown.instance_info), - re_path(r'^api/v1/instance/peers/?$', wellknown.peers), - + re_path(r"^inbox/?$", views.Inbox.as_view()), + re_path(r"%s/inbox/?$" % local_user_path, views.Inbox.as_view()), + re_path(r"%s/outbox/?$" % local_user_path, views.Outbox.as_view()), + re_path(r"^.well-known/webfinger/?$", wellknown.webfinger), + re_path(r"^.well-known/nodeinfo/?$", wellknown.nodeinfo_pointer), + re_path(r"^nodeinfo/2\.0/?$", wellknown.nodeinfo), + re_path(r"^api/v1/instance/?$", wellknown.instance_info), + re_path(r"^api/v1/instance/peers/?$", wellknown.peers), # polling updates - re_path('^api/updates/notifications/?$', views.Updates.as_view()), - + re_path("^api/updates/notifications/?$", views.Updates.as_view()), # authentication - re_path(r'^login/?$', views.Login.as_view()), - re_path(r'^register/?$', views.Register.as_view()), - re_path(r'^logout/?$', views.Logout.as_view()), - re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()), - re_path(r'^password-reset/(?P[A-Za-z0-9]+)/?$', - views.PasswordReset.as_view()), - + re_path(r"^login/?$", views.Login.as_view()), + re_path(r"^register/?$", views.Register.as_view()), + re_path(r"^logout/?$", views.Logout.as_view()), + re_path(r"^password-reset/?$", views.PasswordResetRequest.as_view()), + re_path( + r"^password-reset/(?P[A-Za-z0-9]+)/?$", views.PasswordReset.as_view() + ), # admin - re_path(r'^settings/site-settings', - views.Site.as_view(), name='settings-site'), - re_path(r'^settings/federation', - views.Federation.as_view(), name='settings-federation'), - 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()), - + re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"), + re_path( + r"^settings/federation", views.Federation.as_view(), name="settings-federation" + ), + 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()), # landing pages - re_path(r'^about/?$', views.About.as_view()), - path('', views.Home.as_view()), - re_path(r'^discover/?$', views.Discover.as_view()), - re_path(r'^notifications/?$', views.Notifications.as_view()), - + re_path(r"^about/?$", views.About.as_view()), + path("", views.Home.as_view()), + re_path(r"^discover/?$", views.Discover.as_view()), + 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/(?P%s)?$' % regex.username, - views.DirectMessage.as_view()), - + re_path(r"^(?Phome|local|federated)/?$", views.Feed.as_view()), + re_path(r"^direct-messages/?$", views.DirectMessage.as_view()), + re_path( + r"^direct-messages/(?P%s)?$" % regex.username, + views.DirectMessage.as_view(), + ), # search - re_path(r'^search/?$', views.Search.as_view()), - + re_path(r"^search/?$", views.Search.as_view()), # imports - re_path(r'^import/?$', views.Import.as_view()), - re_path(r'^import/(\d+)/?$', views.ImportStatus.as_view()), - + re_path(r"^import/?$", views.Import.as_view()), + re_path(r"^import/(\d+)/?$", views.ImportStatus.as_view()), # users - re_path(r'%s/?$' % user_path, views.User.as_view(), name='user-feed'), - re_path(r'%s\.json$' % user_path, views.User.as_view()), - re_path(r'%s/rss' % user_path, views.rss_feed.RssFeed(), name='user-rss'), - re_path(r'%s/followers(.json)?/?$' % user_path, - views.Followers.as_view(), name='user-followers'), - re_path(r'%s/following(.json)?/?$' % user_path, - views.Following.as_view(), name='user-following'), - re_path(r'%s/shelves/?$' % user_path, - views.user_shelves_page, name='user-shelves'), - re_path(r'%s/lists/?$' % user_path, - views.UserLists.as_view(), name='user-lists'), - re_path(r'%s/goal/(?P\d{4})/?$' % user_path, - views.Goal.as_view(), name='user-goal'), - - + re_path(r"%s/?$" % user_path, views.User.as_view(), name="user-feed"), + re_path(r"%s\.json$" % user_path, views.User.as_view()), + re_path(r"%s/rss" % user_path, views.rss_feed.RssFeed(), name="user-rss"), + re_path( + r"%s/followers(.json)?/?$" % user_path, + views.Followers.as_view(), + name="user-followers", + ), + re_path( + r"%s/following(.json)?/?$" % user_path, + views.Following.as_view(), + name="user-following", + ), + re_path(r"%s/shelves/?$" % user_path, views.user_shelves_page, name="user-shelves"), + re_path(r"%s/lists/?$" % user_path, views.UserLists.as_view(), name="user-lists"), + re_path( + r"%s/goal/(?P\d{4})/?$" % user_path, + views.Goal.as_view(), + name="user-goal", + ), # lists - re_path(r'^list/?$', views.Lists.as_view(), name='lists'), - re_path(r'^list/(?P\d+)(.json)?/?$', - views.List.as_view(), name='list'), - re_path(r'^list/(?P\d+)/add/?$', - views.list.add_book, name='list-add-book'), - re_path(r'^list/(?P\d+)/remove/?$', - views.list.remove_book, name='list-remove-book'), - re_path(r'^list/(?P\d+)/curate/?$', - views.Curate.as_view(), name='list-curate'), - + re_path(r"^list/?$", views.Lists.as_view(), name="lists"), + re_path(r"^list/(?P\d+)(.json)?/?$", views.List.as_view(), name="list"), + re_path( + r"^list/(?P\d+)/add/?$", views.list.add_book, name="list-add-book" + ), + re_path( + r"^list/(?P\d+)/remove/?$", + views.list.remove_book, + name="list-remove-book", + ), + re_path( + r"^list/(?P\d+)/curate/?$", views.Curate.as_view(), name="list-curate" + ), # preferences - re_path(r'^preferences/profile/?$', - views.EditUser.as_view(), name='prefs-profile'), - re_path(r'^preferences/password/?$', views.ChangePassword.as_view()), - re_path(r'^preferences/block/?$', views.Block.as_view()), - re_path(r'^block/(?P\d+)/?$', views.Block.as_view()), - re_path(r'^unblock/(?P\d+)/?$', views.unblock), - + re_path(r"^preferences/profile/?$", views.EditUser.as_view(), name="prefs-profile"), + re_path(r"^preferences/password/?$", views.ChangePassword.as_view()), + re_path(r"^preferences/block/?$", views.Block.as_view()), + re_path(r"^block/(?P\d+)/?$", views.Block.as_view()), + re_path(r"^unblock/(?P\d+)/?$", views.unblock), # statuses - re_path(r'%s(.json)?/?$' % status_path, views.Status.as_view()), - re_path(r'%s/activity/?$' % status_path, views.Status.as_view()), - re_path(r'%s/replies(.json)?/?$' % status_path, views.Replies.as_view()), - re_path(r'^post/(?P\w+)/?$', views.CreateStatus.as_view()), - re_path(r'^delete-status/(?P\d+)/?$', - views.DeleteStatus.as_view()), - + re_path(r"%s(.json)?/?$" % status_path, views.Status.as_view()), + re_path(r"%s/activity/?$" % status_path, views.Status.as_view()), + re_path(r"%s/replies(.json)?/?$" % status_path, views.Replies.as_view()), + re_path(r"^post/(?P\w+)/?$", views.CreateStatus.as_view()), + re_path(r"^delete-status/(?P\d+)/?$", views.DeleteStatus.as_view()), # interact - re_path(r'^favorite/(?P\d+)/?$', views.Favorite.as_view()), - re_path(r'^unfavorite/(?P\d+)/?$', views.Unfavorite.as_view()), - re_path(r'^boost/(?P\d+)/?$', views.Boost.as_view()), - re_path(r'^unboost/(?P\d+)/?$', views.Unboost.as_view()), - + re_path(r"^favorite/(?P\d+)/?$", views.Favorite.as_view()), + re_path(r"^unfavorite/(?P\d+)/?$", views.Unfavorite.as_view()), + 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/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/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()), - + re_path(r"^isbn/(?P\d+)(.json)?/?$", views.Isbn.as_view()), # author - re_path(r'^author/(?P\d+)(.json)?/?$', views.Author.as_view()), - re_path(r'^author/(?P\d+)/edit/?$', views.EditAuthor.as_view()), - + re_path(r"^author/(?P\d+)(.json)?/?$", views.Author.as_view()), + re_path(r"^author/(?P\d+)/edit/?$", views.EditAuthor.as_view()), # tags - re_path(r'^tag/(?P.+)\.json/?$', views.Tag.as_view()), - re_path(r'^tag/(?P.+)/?$', views.Tag.as_view()), - re_path(r'^tag/?$', views.AddTag.as_view()), - re_path(r'^untag/?$', views.RemoveTag.as_view()), - + re_path(r"^tag/(?P.+)\.json/?$", views.Tag.as_view()), + re_path(r"^tag/(?P.+)/?$", views.Tag.as_view()), + re_path(r"^tag/?$", views.AddTag.as_view()), + re_path(r"^untag/?$", views.RemoveTag.as_view()), # shelf - re_path(r'^%s/shelf/(?P[\w-]+)(.json)?/?$' % \ - user_path, views.Shelf.as_view(), name='shelf'), - re_path(r'^%s/shelf/(?P[\w-]+)(.json)?/?$' % \ - local_user_path, views.Shelf.as_view()), - re_path(r'^create-shelf/?$', views.create_shelf, name='shelf-create'), - re_path(r'^delete-shelf/(?P\d+)?$', views.delete_shelf), - re_path(r'^shelve/?$', views.shelve), - re_path(r'^unshelve/?$', views.unshelve), - + re_path( + r"^%s/shelf/(?P[\w-]+)(.json)?/?$" % user_path, + views.Shelf.as_view(), + name="shelf", + ), + re_path( + r"^%s/shelf/(?P[\w-]+)(.json)?/?$" % local_user_path, + views.Shelf.as_view(), + ), + re_path(r"^create-shelf/?$", views.create_shelf, name="shelf-create"), + re_path(r"^delete-shelf/(?P\d+)?$", views.delete_shelf), + re_path(r"^shelve/?$", views.shelve), + re_path(r"^unshelve/?$", views.unshelve), # reading progress - re_path(r'^edit-readthrough/?$', views.edit_readthrough), - re_path(r'^delete-readthrough/?$', views.delete_readthrough), - re_path(r'^create-readthrough/?$', views.create_readthrough), - re_path(r'^delete-progressupdate/?$', views.delete_progressupdate), - - re_path(r'^start-reading/(?P\d+)/?$', views.start_reading), - re_path(r'^finish-reading/(?P\d+)/?$', views.finish_reading), - + re_path(r"^edit-readthrough/?$", views.edit_readthrough), + re_path(r"^delete-readthrough/?$", views.delete_readthrough), + re_path(r"^create-readthrough/?$", views.create_readthrough), + re_path(r"^delete-progressupdate/?$", views.delete_progressupdate), + re_path(r"^start-reading/(?P\d+)/?$", views.start_reading), + re_path(r"^finish-reading/(?P\d+)/?$", views.finish_reading), # following - re_path(r'^follow/?$', views.follow), - re_path(r'^unfollow/?$', views.unfollow), - re_path(r'^accept-follow-request/?$', views.accept_follow_request), - re_path(r'^delete-follow-request/?$', views.delete_follow_request), - + re_path(r"^follow/?$", views.follow), + re_path(r"^unfollow/?$", views.unfollow), + re_path(r"^accept-follow-request/?$", views.accept_follow_request), + re_path(r"^delete-follow-request/?$", views.delete_follow_request), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/bookwyrm/utils/regex.py b/bookwyrm/utils/regex.py index c818bc415..6389c35d6 100644 --- a/bookwyrm/utils/regex.py +++ b/bookwyrm/utils/regex.py @@ -1,10 +1,10 @@ -''' defining regexes for regularly used concepts ''' +""" defining regexes for regularly used concepts """ -domain = r'[\w_\-\.]+\.[a-z]{2,}' -localname = r'@?[a-zA-Z_\-\.0-9]+' -strict_localname = r'@[a-zA-Z_\-\.0-9]+' -username = r'%s(@%s)?' % (localname, domain) -strict_username = r'\B%s(@%s)?\b' % (strict_localname, domain) -full_username = r'%s@%s\b' % (localname, domain) +domain = r"[\w_\-\.]+\.[a-z]{2,}" +localname = r"@?[a-zA-Z_\-\.0-9]+" +strict_localname = r"@[a-zA-Z_\-\.0-9]+" +username = r"%s(@%s)?" % (localname, domain) +strict_username = r"\B%s(@%s)?\b" % (strict_localname, domain) +full_username = r"%s@%s\b" % (localname, domain) # should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2; -bookwyrm_user_agent = r'\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;' +bookwyrm_user_agent = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;" diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index dd601b28b..48da8ec1a 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -1,4 +1,4 @@ -''' make sure all our nice views are available ''' +""" make sure all our nice views are available """ from .authentication import Login, Register, Logout from .author import Author, EditAuthor from .block import Block, unblock diff --git a/bookwyrm/views/authentication.py b/bookwyrm/views/authentication.py index 13c9ce49e..a91475567 100644 --- a/bookwyrm/views/authentication.py +++ b/bookwyrm/views/authentication.py @@ -1,4 +1,4 @@ -''' class views for login/register views ''' +""" class views for login/register views """ from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied @@ -14,60 +14,59 @@ from bookwyrm.settings import DOMAIN # pylint: disable= no-self-use -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class Login(View): - ''' authenticate an existing user ''' + """ authenticate an existing user """ + def get(self, request): - ''' login page ''' + """ login page """ if request.user.is_authenticated: - return redirect('/') + return redirect("/") # sene user to the login page data = { - 'login_form': forms.LoginForm(), - 'register_form': forms.RegisterForm(), + "login_form": forms.LoginForm(), + "register_form": forms.RegisterForm(), } - return TemplateResponse(request, 'login.html', data) + return TemplateResponse(request, "login.html", data) def post(self, request): - ''' authentication action ''' + """ authentication action """ if request.user.is_authenticated: - return redirect('/') + return redirect("/") login_form = forms.LoginForm(request.POST) - localname = login_form.data['localname'] - if '@' in localname: # looks like an email address to me + localname = login_form.data["localname"] + if "@" in localname: # looks like an email address to me email = localname try: username = models.User.objects.get(email=email) - except models.User.DoesNotExist: # maybe it's a full username? + except models.User.DoesNotExist: # maybe it's a full username? username = localname else: - username = '%s@%s' % (localname, DOMAIN) - password = login_form.data['password'] + username = "%s@%s" % (localname, DOMAIN) + password = login_form.data["password"] user = authenticate(request, username=username, password=password) if user is not None: # successful login login(request, user) user.last_active_date = timezone.now() user.save(broadcast=False) - return redirect(request.GET.get('next', '/')) + return redirect(request.GET.get("next", "/")) # login errors - login_form.non_field_errors = 'Username or password are incorrect' + login_form.non_field_errors = "Username or password are incorrect" register_form = forms.RegisterForm() - data = { - 'login_form': login_form, - 'register_form': register_form - } - return TemplateResponse(request, 'login.html', data) + data = {"login_form": login_form, "register_form": register_form} + return TemplateResponse(request, "login.html", data) class Register(View): - ''' register a user ''' + """ register a user """ + def post(self, request): - ''' join the server ''' + """ join the server """ if not models.SiteSettings.get().allow_registration: - invite_code = request.POST.get('invite_code') + invite_code = request.POST.get("invite_code") if not invite_code: raise PermissionDenied @@ -83,42 +82,43 @@ class Register(View): if not form.is_valid(): errors = True - localname = form.data['localname'].strip() - email = form.data['email'] - password = form.data['password'] + localname = form.data["localname"].strip() + email = form.data["email"] + password = form.data["password"] # check localname and email uniqueness if models.User.objects.filter(localname=localname).first(): - form.errors['localname'] = [ - 'User with this username already exists'] + form.errors["localname"] = ["User with this username already exists"] errors = True if errors: data = { - 'login_form': forms.LoginForm(), - 'register_form': form, - 'invite': invite, - 'valid': invite.valid() if invite else True, + "login_form": forms.LoginForm(), + "register_form": form, + "invite": invite, + "valid": invite.valid() if invite else True, } if invite: - return TemplateResponse(request, 'invite.html', data) - return TemplateResponse(request, 'login.html', data) + return TemplateResponse(request, "invite.html", data) + return TemplateResponse(request, "login.html", data) - username = '%s@%s' % (localname, DOMAIN) + username = "%s@%s" % (localname, DOMAIN) user = models.User.objects.create_user( - username, email, password, localname=localname, local=True) + username, email, password, localname=localname, local=True + ) if invite: invite.times_used += 1 invite.save() login(request, user) - return redirect('/') + return redirect("/") -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Logout(View): - ''' log out ''' + """ log out """ + def get(self, request): - ''' done with this place! outa here! ''' + """ done with this place! outa here! """ logout(request) - return redirect('/') + return redirect("/") diff --git a/bookwyrm/views/author.py b/bookwyrm/views/author.py index 356c95ffd..50a3588de 100644 --- a/bookwyrm/views/author.py +++ b/bookwyrm/views/author.py @@ -1,4 +1,4 @@ -''' the good people stuff! the authors! ''' +""" the good people stuff! the authors! """ from django.contrib.auth.decorators import login_required, permission_required from django.db.models import Q from django.shortcuts import get_object_or_404, redirect @@ -13,49 +13,46 @@ from .helpers import is_api_request # pylint: disable= no-self-use class Author(View): - ''' this person wrote a book ''' + """ this person wrote a book """ + def get(self, request, author_id): - ''' landing page for an author ''' + """ landing page for an author """ author = get_object_or_404(models.Author, id=author_id) if is_api_request(request): return ActivitypubResponse(author.to_activity()) books = models.Work.objects.filter( - Q(authors=author) | Q(editions__authors=author)).distinct() + Q(authors=author) | Q(editions__authors=author) + ).distinct() data = { - 'author': author, - 'books': [b.get_default_edition() for b in books], + "author": author, + "books": [b.get_default_edition() for b in books], } - return TemplateResponse(request, 'author.html', data) + return TemplateResponse(request, "author.html", data) -@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 EditAuthor(View): - ''' edit author info ''' + """ edit author info """ + def get(self, request, author_id): - ''' info about a book ''' + """ info about a book """ author = get_object_or_404(models.Author, id=author_id) - data = { - 'author': author, - 'form': forms.AuthorForm(instance=author) - } - return TemplateResponse(request, 'edit_author.html', data) + data = {"author": author, "form": forms.AuthorForm(instance=author)} + return TemplateResponse(request, "edit_author.html", data) def post(self, request, author_id): - ''' edit a author cool ''' + """ edit a author cool """ author = get_object_or_404(models.Author, id=author_id) form = forms.AuthorForm(request.POST, request.FILES, instance=author) if not form.is_valid(): - data = { - 'author': author, - 'form': form - } - return TemplateResponse(request, 'edit_author.html', data) + data = {"author": author, "form": form} + return TemplateResponse(request, "edit_author.html", data) author = form.save() - return redirect('/author/%s' % author.id) + return redirect("/author/%s" % author.id) diff --git a/bookwyrm/views/block.py b/bookwyrm/views/block.py index 90e5033b5..6d6a8a58c 100644 --- a/bookwyrm/views/block.py +++ b/bookwyrm/views/block.py @@ -1,4 +1,4 @@ -''' views for actions you can take in the application ''' +""" views for actions you can take in the application """ from django.contrib.auth.decorators import login_required from django.http import HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect @@ -10,25 +10,27 @@ from django.views.decorators.http import require_POST from bookwyrm import models # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Block(View): - ''' blocking users ''' + """ blocking users """ + def get(self, request): - ''' list of blocked users? ''' - return TemplateResponse(request, 'preferences/blocks.html') + """ list of blocked users? """ + return TemplateResponse(request, "preferences/blocks.html") def post(self, request, user_id): - ''' block a user ''' + """ block a user """ to_block = get_object_or_404(models.User, id=user_id) models.UserBlocks.objects.create( - user_subject=request.user, user_object=to_block) - return redirect('/preferences/block') + user_subject=request.user, user_object=to_block + ) + return redirect("/preferences/block") @require_POST @login_required def unblock(request, user_id): - ''' undo a block ''' + """ undo a block """ to_unblock = get_object_or_404(models.User, id=user_id) try: block = models.UserBlocks.objects.get( @@ -38,4 +40,4 @@ def unblock(request, user_id): except models.UserBlocks.DoesNotExist: return HttpResponseNotFound() block.delete() - return redirect('/preferences/block') + return redirect("/preferences/block") diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index cf246446c..46df04830 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -1,4 +1,4 @@ -''' the good stuff! the books! ''' +""" the good stuff! the books! """ from django.core.paginator import Paginator from django.contrib.auth.decorators import login_required, permission_required from django.db import transaction @@ -20,11 +20,12 @@ from .helpers import privacy_filter # pylint: disable= no-self-use class Book(View): - ''' a book! this is the stuff ''' + """ a book! this is the stuff """ + def get(self, request, book_id): - ''' info about a book ''' + """ info about a book """ try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 @@ -50,30 +51,28 @@ class Book(View): reviews = get_activity_feed(request.user, queryset=reviews) # the reviews to show - paginated = Paginator(reviews.exclude( - Q(content__isnull=True) | Q(content='') - ), PAGE_LENGTH) + paginated = Paginator( + reviews.exclude(Q(content__isnull=True) | Q(content="")), PAGE_LENGTH + ) reviews_page = paginated.page(page) user_tags = readthroughs = user_shelves = other_edition_shelves = [] if request.user.is_authenticated: user_tags = models.UserTag.objects.filter( book=book, user=request.user - ).values_list('tag__identifier', flat=True) + ).values_list("tag__identifier", flat=True) readthroughs = models.ReadThrough.objects.filter( user=request.user, book=book, - ).order_by('start_date') + ).order_by("start_date") for readthrough in readthroughs: - readthrough.progress_updates = \ - readthrough.progressupdate_set.all() \ - .order_by('-updated_date') + readthrough.progress_updates = ( + readthrough.progressupdate_set.all().order_by("-updated_date") + ) - user_shelves = models.ShelfBook.objects.filter( - user=request.user, book=book - ) + user_shelves = models.ShelfBook.objects.filter(user=request.user, book=book) other_edition_shelves = models.ShelfBook.objects.filter( ~Q(book=book), @@ -82,129 +81,124 @@ class Book(View): ) data = { - 'book': book, - 'reviews': reviews_page, - 'review_count': reviews.count(), - 'ratings': reviews.filter(Q(content__isnull=True) | Q(content='')), - 'rating': reviews.aggregate(Avg('rating'))['rating__avg'], - 'tags': models.UserTag.objects.filter(book=book), - 'lists': privacy_filter( + "book": book, + "reviews": reviews_page, + "review_count": reviews.count(), + "ratings": reviews.filter(Q(content__isnull=True) | Q(content="")), + "rating": reviews.aggregate(Avg("rating"))["rating__avg"], + "tags": models.UserTag.objects.filter(book=book), + "lists": privacy_filter( request.user, book.list_set.filter(listitem__approved=True) ), - 'user_tags': user_tags, - 'user_shelves': user_shelves, - 'other_edition_shelves': other_edition_shelves, - 'readthroughs': readthroughs, - 'path': '/book/%s' % book_id, + "user_tags": user_tags, + "user_shelves": user_shelves, + "other_edition_shelves": other_edition_shelves, + "readthroughs": readthroughs, + "path": "/book/%s" % book_id, } - return TemplateResponse(request, 'book.html', data) + return TemplateResponse(request, "book.html", data) -@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 EditBook(View): - ''' edit a book ''' + """ edit a book """ + def get(self, request, book_id): - ''' info about a book ''' + """ info about a book """ 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): - ''' edit a book cool ''' + """ edit a book cool """ book = get_object_or_404(models.Edition, id=book_id) form = forms.EditionForm(request.POST, request.FILES, instance=book) if not form.is_valid(): - data = { - 'book': book, - 'form': form - } - return TemplateResponse(request, 'edit_book.html', data) + data = {"book": book, "form": form} + return TemplateResponse(request, "edit_book.html", data) book = form.save() - return redirect('/book/%s' % book.id) + return redirect("/book/%s" % book.id) class Editions(View): - ''' list of editions ''' + """ list of editions """ + def get(self, request, book_id): - ''' list of editions of a book ''' + """ list of editions of a book """ work = get_object_or_404(models.Work, id=book_id) if is_api_request(request): return ActivitypubResponse(work.to_edition_list(**request.GET)) data = { - 'editions': work.editions.order_by('-edition_rank').all(), - 'work': work, + "editions": work.editions.order_by("-edition_rank").all(), + "work": work, } - return TemplateResponse(request, 'editions.html', data) + return TemplateResponse(request, "editions.html", data) @login_required @require_POST def upload_cover(request, book_id): - ''' upload a new cover ''' + """ upload a new cover """ book = get_object_or_404(models.Edition, id=book_id) form = forms.CoverForm(request.POST, request.FILES, instance=book) if not form.is_valid(): - return redirect('/book/%d' % book.id) + return redirect("/book/%d" % book.id) book.last_edited_by = request.user - book.cover = form.files['cover'] + book.cover = form.files["cover"] book.save() - return redirect('/book/%s' % book.id) + return redirect("/book/%s" % book.id) @login_required @require_POST -@permission_required('bookwyrm.edit_book', raise_exception=True) +@permission_required("bookwyrm.edit_book", raise_exception=True) def add_description(request, book_id): - ''' upload a new cover ''' - if not request.method == 'POST': - return redirect('/') + """ upload a new cover """ + if not request.method == "POST": + return redirect("/") book = get_object_or_404(models.Edition, id=book_id) - description = request.POST.get('description') + description = request.POST.get("description") book.description = description book.last_edited_by = request.user book.save() - return redirect('/book/%s' % book.id) + return redirect("/book/%s" % book.id) @require_POST def resolve_book(request): - ''' figure out the local path to a book from a remote_id ''' - remote_id = request.POST.get('remote_id') + """ figure out the local path to a book from a remote_id """ + remote_id = request.POST.get("remote_id") connector = connector_manager.get_or_create_connector(remote_id) book = connector.get_or_create_book(remote_id) - return redirect('/book/%d' % book.id) + return redirect("/book/%d" % book.id) @login_required @require_POST @transaction.atomic def switch_edition(request): - ''' switch your copy of a book to a different edition ''' - edition_id = request.POST.get('edition') + """ switch your copy of a book to a different edition """ + edition_id = request.POST.get("edition") new_edition = get_object_or_404(models.Edition, id=edition_id) shelfbooks = models.ShelfBook.objects.filter( - book__parent_work=new_edition.parent_work, - shelf__user=request.user + book__parent_work=new_edition.parent_work, shelf__user=request.user ) for shelfbook in shelfbooks.all(): with transaction.atomic(): @@ -212,16 +206,15 @@ def switch_edition(request): created_date=shelfbook.created_date, user=shelfbook.user, shelf=shelfbook.shelf, - book=new_edition + book=new_edition, ) shelfbook.delete() readthroughs = models.ReadThrough.objects.filter( - book__parent_work=new_edition.parent_work, - user=request.user + book__parent_work=new_edition.parent_work, user=request.user ) for readthrough in readthroughs.all(): readthrough.book = new_edition readthrough.save() - return redirect('/book/%d' % new_edition.id) + return redirect("/book/%d" % new_edition.id) diff --git a/bookwyrm/views/error.py b/bookwyrm/views/error.py index 82999d6ed..9bd94ca32 100644 --- a/bookwyrm/views/error.py +++ b/bookwyrm/views/error.py @@ -1,11 +1,12 @@ -''' something has gone amiss ''' +""" something has gone amiss """ from django.template.response import TemplateResponse + def server_error_page(request): - ''' 500 errors ''' - return TemplateResponse(request, 'error.html', status=500) + """ 500 errors """ + return TemplateResponse(request, "error.html", status=500) def not_found_page(request, _): - ''' 404s ''' - return TemplateResponse(request, 'notfound.html', status=404) + """ 404s """ + return TemplateResponse(request, "notfound.html", status=404) diff --git a/bookwyrm/views/federation.py b/bookwyrm/views/federation.py index 62ae076cd..e9c0466e4 100644 --- a/bookwyrm/views/federation.py +++ b/bookwyrm/views/federation.py @@ -1,4 +1,4 @@ -''' manage federated servers ''' +""" manage federated servers """ from django.contrib.auth.decorators import login_required, permission_required from django.template.response import TemplateResponse from django.utils.decorators import method_decorator @@ -8,14 +8,16 @@ from bookwyrm import models # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") @method_decorator( - permission_required('bookwyrm.control_federation', raise_exception=True), - name='dispatch') + permission_required("bookwyrm.control_federation", raise_exception=True), + name="dispatch", +) class Federation(View): - ''' what servers do we federate with ''' + """ what servers do we federate with """ + def get(self, request): - ''' edit form ''' + """ edit form """ servers = models.FederatedServer.objects.all() - data = {'servers': servers} - return TemplateResponse(request, 'settings/federation.html', data) + data = {"servers": servers} + return TemplateResponse(request, "settings/federation.html", data) diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index f7e93e9a3..d08c9a42c 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -1,4 +1,4 @@ -''' non-interactive pages ''' +""" non-interactive pages """ from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db.models import Q @@ -17,48 +17,54 @@ from .helpers import is_api_request, is_bookwyrm_request, object_visible_to_user # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Feed(View): - ''' activity stream ''' + """ activity stream """ + def get(self, request, tab): - ''' user's homepage with activity feed ''' + """ user's homepage with activity feed """ try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 - if tab == 'home': + if tab == "home": + activities = get_activity_feed(request.user, following_only=True) + tab_title = _("Home") + elif tab == "local": activities = get_activity_feed( - request.user, following_only=True) - tab_title = _('Home') - elif tab == 'local': - activities = get_activity_feed( - request.user, privacy=['public', 'followers'], local_only=True) - tab_title = _('Local') + request.user, privacy=["public", "followers"], local_only=True + ) + tab_title = _("Local") else: activities = get_activity_feed( - request.user, privacy=['public', 'followers']) - tab_title = _('Federated') + request.user, privacy=["public", "followers"] + ) + tab_title = _("Federated") paginated = Paginator(activities, PAGE_LENGTH) - data = {**feed_page_data(request.user), **{ - 'user': request.user, - 'activities': paginated.page(page), - 'tab': tab, - 'tab_title': tab_title, - 'goal_form': forms.GoalForm(), - 'path': '/%s' % tab, - }} - return TemplateResponse(request, 'feed/feed.html', data) + data = { + **feed_page_data(request.user), + **{ + "user": request.user, + "activities": paginated.page(page), + "tab": tab, + "tab_title": tab_title, + "goal_form": forms.GoalForm(), + "path": "/%s" % tab, + }, + } + return TemplateResponse(request, "feed/feed.html", data) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class DirectMessage(View): - ''' dm view ''' + """ dm view """ + def get(self, request, username=None): - ''' like a feed but for dms only ''' + """ like a feed but for dms only """ try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 @@ -74,27 +80,33 @@ class DirectMessage(View): queryset = queryset.filter(Q(user=user) | Q(mention_users=user)) activities = get_activity_feed( - request.user, privacy=['direct'], queryset=queryset) + request.user, privacy=["direct"], queryset=queryset + ) paginated = Paginator(activities, PAGE_LENGTH) activity_page = paginated.page(page) - data = {**feed_page_data(request.user), **{ - 'user': request.user, - 'partner': user, - 'activities': activity_page, - 'path': '/direct-messages', - }} - return TemplateResponse(request, 'feed/direct_messages.html', data) + data = { + **feed_page_data(request.user), + **{ + "user": request.user, + "partner": user, + "activities": activity_page, + "path": "/direct-messages", + }, + } + return TemplateResponse(request, "feed/direct_messages.html", data) class Status(View): - ''' get posting ''' + """ get posting """ + def get(self, request, username, status_id): - ''' display a particular status (and replies, etc) ''' + """ display a particular status (and replies, etc) """ try: user = get_user_from_username(request.user, username) status = models.Status.objects.select_subclasses().get( - id=status_id, deleted=False) + id=status_id, deleted=False + ) except ValueError: return HttpResponseNotFound() @@ -108,18 +120,23 @@ class Status(View): if is_api_request(request): return ActivitypubResponse( - status.to_activity(pure=not is_bookwyrm_request(request))) + status.to_activity(pure=not is_bookwyrm_request(request)) + ) - data = {**feed_page_data(request.user), **{ - 'status': status, - }} - return TemplateResponse(request, 'feed/status.html', data) + data = { + **feed_page_data(request.user), + **{ + "status": status, + }, + } + return TemplateResponse(request, "feed/status.html", data) class Replies(View): - ''' replies page (a json view of status) ''' + """ replies page (a json view of status) """ + def get(self, request, username, status_id): - ''' ordered collection of replies to a status ''' + """ ordered collection of replies to a status """ # the html view is the same as Status if not is_api_request(request): status_view = Status.as_view() @@ -134,41 +151,39 @@ class Replies(View): def feed_page_data(user): - ''' info we need for every feed page ''' + """ info we need for every feed page """ if not user.is_authenticated: return {} - goal = models.AnnualGoal.objects.filter( - user=user, year=timezone.now().year - ).first() + goal = models.AnnualGoal.objects.filter(user=user, year=timezone.now().year).first() return { - 'suggested_books': get_suggested_books(user), - 'goal': goal, - 'goal_form': forms.GoalForm(), + "suggested_books": get_suggested_books(user), + "goal": goal, + "goal_form": forms.GoalForm(), } + def get_suggested_books(user, max_books=5): - ''' helper to get a user's recent books ''' + """ helper to get a user's recent books """ book_count = 0 - preset_shelves = [ - ('reading', max_books), ('read', 2), ('to-read', max_books) - ] + preset_shelves = [("reading", max_books), ("read", 2), ("to-read", max_books)] suggested_books = [] for (preset, shelf_max) in preset_shelves: - limit = shelf_max if shelf_max < (max_books - book_count) \ - else max_books - book_count + limit = ( + shelf_max + if shelf_max < (max_books - book_count) + else max_books - book_count + ) shelf = user.shelf_set.get(identifier=preset) - shelf_books = shelf.shelfbook_set.order_by( - '-updated_date' - ).all()[:limit] + shelf_books = shelf.shelfbook_set.order_by("-updated_date").all()[:limit] if not shelf_books: continue shelf_preview = { - 'name': shelf.name, - 'identifier': shelf.identifier, - 'books': [s.book for s in shelf_books] + "name": shelf.name, + "identifier": shelf.identifier, + "books": [s.book for s in shelf_books], } suggested_books.append(shelf_preview) - book_count += len(shelf_preview['books']) + book_count += len(shelf_preview["books"]) return suggested_books diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py index 4c69890c9..515bf3251 100644 --- a/bookwyrm/views/follow.py +++ b/bookwyrm/views/follow.py @@ -1,4 +1,4 @@ -''' views for actions you can take in the application ''' +""" views for actions you can take in the application """ from django.contrib.auth.decorators import login_required from django.db import IntegrityError from django.http import HttpResponseBadRequest @@ -8,11 +8,12 @@ from django.views.decorators.http import require_POST from bookwyrm import models from .helpers import get_user_from_username + @login_required @require_POST def follow(request): - ''' follow another user, here or abroad ''' - username = request.POST['user'] + """ follow another user, here or abroad """ + username = request.POST["user"] try: to_follow = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -32,16 +33,15 @@ def follow(request): @login_required @require_POST def unfollow(request): - ''' unfollow a user ''' - username = request.POST['user'] + """ unfollow a user """ + username = request.POST["user"] try: to_unfollow = get_user_from_username(request.user, username) except models.User.DoesNotExist: return HttpResponseBadRequest() models.UserFollows.objects.get( - user_subject=request.user, - user_object=to_unfollow + user_subject=request.user, user_object=to_unfollow ).delete() return redirect(to_unfollow.local_path) @@ -49,8 +49,8 @@ def unfollow(request): @login_required @require_POST def accept_follow_request(request): - ''' a user accepts a follow request ''' - username = request.POST['user'] + """ a user accepts a follow request """ + username = request.POST["user"] try: requester = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -58,8 +58,7 @@ def accept_follow_request(request): try: follow_request = models.UserFollowRequest.objects.get( - user_subject=requester, - user_object=request.user + user_subject=requester, user_object=request.user ) except models.UserFollowRequest.DoesNotExist: # Request already dealt with. @@ -72,8 +71,8 @@ def accept_follow_request(request): @login_required @require_POST def delete_follow_request(request): - ''' a user rejects a follow request ''' - username = request.POST['user'] + """ a user rejects a follow request """ + username = request.POST["user"] try: requester = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -81,11 +80,10 @@ def delete_follow_request(request): try: follow_request = models.UserFollowRequest.objects.get( - user_subject=requester, - user_object=request.user + user_subject=requester, user_object=request.user ) except models.UserFollowRequest.DoesNotExist: return HttpResponseBadRequest() follow_request.delete() - return redirect('/user/%s' % request.user.localname) + return redirect("/user/%s" % request.user.localname) diff --git a/bookwyrm/views/goal.py b/bookwyrm/views/goal.py index 7da9e4343..4e3a9534c 100644 --- a/bookwyrm/views/goal.py +++ b/bookwyrm/views/goal.py @@ -1,4 +1,4 @@ -''' non-interactive pages ''' +""" non-interactive pages """ from django.contrib.auth.decorators import login_required from django.http import HttpResponseNotFound from django.shortcuts import redirect @@ -13,16 +13,15 @@ from .helpers import get_user_from_username, object_visible_to_user # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Goal(View): - ''' track books for the year ''' + """ track books for the year """ + def get(self, request, username, year): - ''' reading goal page ''' + """ reading goal page """ user = get_user_from_username(request.user, username) year = int(year) - goal = models.AnnualGoal.objects.filter( - year=year, user=user - ).first() + goal = models.AnnualGoal.objects.filter(year=year, user=user).first() if not goal and user != request.user: return HttpResponseNotFound() @@ -30,42 +29,39 @@ class Goal(View): return HttpResponseNotFound() data = { - 'goal_form': forms.GoalForm(instance=goal), - 'goal': goal, - 'user': user, - 'year': year, - 'is_self': request.user == user, + "goal_form": forms.GoalForm(instance=goal), + "goal": goal, + "user": user, + "year": year, + "is_self": request.user == user, } - return TemplateResponse(request, 'goal.html', data) - + return TemplateResponse(request, "goal.html", data) def post(self, request, username, year): - ''' update or create an annual goal ''' + """ update or create an annual goal """ user = get_user_from_username(request.user, username) if user != request.user: return HttpResponseNotFound() year = int(year) - goal = models.AnnualGoal.objects.filter( - year=year, user=request.user - ).first() + goal = models.AnnualGoal.objects.filter(year=year, user=request.user).first() form = forms.GoalForm(request.POST, instance=goal) if not form.is_valid(): data = { - 'goal_form': form, - 'goal': goal, - 'year': year, + "goal_form": form, + "goal": goal, + "year": year, } - return TemplateResponse(request, 'goal.html', data) + return TemplateResponse(request, "goal.html", data) goal = form.save() - if request.POST.get('post-status'): + if request.POST.get("post-status"): # create status, if appropraite - template = get_template('snippets/generated_status/goal.html') + template = get_template("snippets/generated_status/goal.html") create_generated_note( request.user, - template.render({'goal': goal, 'user': request.user}).strip(), - privacy=goal.privacy + template.render({"goal": goal, "user": request.user}).strip(), + privacy=goal.privacy, ) - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 64f0fc267..20332cbda 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -1,4 +1,4 @@ -''' helper functions used in various views ''' +""" helper functions used in various views """ import re from requests import HTTPError from django.core.exceptions import FieldError @@ -11,7 +11,7 @@ from bookwyrm.utils import regex def get_user_from_username(viewer, username): - ''' helper function to resolve a localname or a username to a user ''' + """ helper function to resolve a localname or a username to a user """ # raises DoesNotExist if user is now found try: return models.User.viewer_aware_objects(viewer).get(localname=username) @@ -20,22 +20,20 @@ def get_user_from_username(viewer, username): def is_api_request(request): - ''' check whether a request is asking for html or data ''' - return 'json' in request.headers.get('Accept') or \ - request.path[-5:] == '.json' + """ check whether a request is asking for html or data """ + return "json" in request.headers.get("Accept") or request.path[-5:] == ".json" def is_bookwyrm_request(request): - ''' check if the request is coming from another bookwyrm instance ''' - user_agent = request.headers.get('User-Agent') - if user_agent is None or \ - re.search(regex.bookwyrm_user_agent, user_agent) is None: + """ check if the request is coming from another bookwyrm instance """ + user_agent = request.headers.get("User-Agent") + if user_agent is None or re.search(regex.bookwyrm_user_agent, user_agent) is None: return False return True def object_visible_to_user(viewer, obj): - ''' is a user authorized to view an object? ''' + """ is a user authorized to view an object? """ if not obj: return False @@ -44,37 +42,32 @@ def object_visible_to_user(viewer, obj): return False # you can see your own posts and any public or unlisted posts - if viewer == obj.user or obj.privacy in ['public', 'unlisted']: + if viewer == obj.user or obj.privacy in ["public", "unlisted"]: return True # you can see the followers only posts of people you follow - if obj.privacy == 'followers' and \ - obj.user.followers.filter(id=viewer.id).first(): + if obj.privacy == "followers" and obj.user.followers.filter(id=viewer.id).first(): return True # you can see dms you are tagged in if isinstance(obj, models.Status): - if obj.privacy == 'direct' and \ - obj.mention_users.filter(id=viewer.id).first(): + if obj.privacy == "direct" and obj.mention_users.filter(id=viewer.id).first(): return True return False def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False): - ''' filter objects that have "user" and "privacy" fields ''' - privacy_levels = privacy_levels or \ - ['public', 'unlisted', 'followers', 'direct'] + """ filter objects that have "user" and "privacy" fields """ + privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"] # exclude blocks from both directions if not viewer.is_anonymous: blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all() - queryset = queryset.exclude( - Q(user__in=blocked) | Q(user__blocks=viewer)) + queryset = queryset.exclude(Q(user__in=blocked) | Q(user__blocks=viewer)) # you can't see followers only or direct messages if you're not logged in if viewer.is_anonymous: - privacy_levels = [p for p in privacy_levels if \ - not p in ['followers', 'direct']] + privacy_levels = [p for p in privacy_levels if not p in ["followers", "direct"]] # filter to only privided privacy levels queryset = queryset.filter(privacy__in=privacy_levels) @@ -82,53 +75,48 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False): # only include statuses the user follows if following_only: queryset = queryset.exclude( - ~Q(# remove everythign except - Q(user__in=viewer.following.all()) | # user following - Q(user=viewer) |# is self - Q(mention_users=viewer)# mentions user + ~Q( # remove everythign except + Q(user__in=viewer.following.all()) + | Q(user=viewer) # user following + | Q(mention_users=viewer) # is self # mentions user ), ) # exclude followers-only statuses the user doesn't follow - elif 'followers' in privacy_levels: + elif "followers" in privacy_levels: queryset = queryset.exclude( - ~Q(# user isn't following and it isn't their own status + ~Q( # user isn't following and it isn't their own status Q(user__in=viewer.following.all()) | Q(user=viewer) ), - privacy='followers' # and the status is followers only + privacy="followers", # and the status is followers only ) # exclude direct messages not intended for the user - if 'direct' in privacy_levels: + if "direct" in privacy_levels: try: queryset = queryset.exclude( - ~Q( - Q(user=viewer) | Q(mention_users=viewer) - ), privacy='direct' + ~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy="direct" ) except FieldError: - queryset = queryset.exclude( - ~Q(user=viewer), privacy='direct' - ) + queryset = queryset.exclude(~Q(user=viewer), privacy="direct") return queryset def get_activity_feed( - user, privacy=None, local_only=False, following_only=False, - queryset=None): - ''' get a filtered queryset of statuses ''' + user, privacy=None, local_only=False, following_only=False, queryset=None +): + """ get a filtered queryset of statuses """ if queryset is None: queryset = models.Status.objects.select_subclasses() # exclude deleted - queryset = queryset.exclude(deleted=True).order_by('-published_date') + queryset = queryset.exclude(deleted=True).order_by("-published_date") # apply privacy filters - queryset = privacy_filter( - user, queryset, privacy, following_only=following_only) + queryset = privacy_filter(user, queryset, privacy, following_only=following_only) # only show dms if we only want dms - if privacy == ['direct']: + if privacy == ["direct"]: # dms are direct statuses not related to books queryset = queryset.filter( review__isnull=True, @@ -143,7 +131,7 @@ def get_activity_feed( comment__isnull=True, quotation__isnull=True, generatednote__isnull=True, - privacy='direct' + privacy="direct", ) except FieldError: # if we're looking at a subtype of Status (like Review) @@ -163,36 +151,35 @@ def get_activity_feed( def handle_remote_webfinger(query): - ''' webfingerin' other servers ''' + """ webfingerin' other servers """ user = None # usernames could be @user@domain or user@domain if not query: return None - if query[0] == '@': + if query[0] == "@": query = query[1:] try: - domain = query.split('@')[1] + domain = query.split("@")[1] except IndexError: return None try: user = models.User.objects.get(username=query) except models.User.DoesNotExist: - url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \ - (domain, query) + url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query) try: data = get_data(url) except (ConnectorException, HTTPError): return None - for link in data.get('links'): - if link.get('rel') == 'self': + for link in data.get("links"): + if link.get("rel") == "self": try: user = activitypub.resolve_remote_id( - link['href'], model=models.User + link["href"], model=models.User ) except KeyError: return None @@ -200,7 +187,7 @@ def handle_remote_webfinger(query): def get_edition(book_id): - ''' look up a book in the db and return an edition ''' + """ look up a book in the db and return an edition """ book = models.Book.objects.select_subclasses().get(id=book_id) if isinstance(book, models.Work): book = book.get_default_edition() @@ -208,29 +195,24 @@ def get_edition(book_id): def handle_reading_status(user, shelf, book, privacy): - ''' post about a user reading a book ''' + """ post about a user reading a book """ # tell the world about this cool thing that happened try: message = { - 'to-read': 'wants to read', - 'reading': 'started reading', - 'read': 'finished reading' + "to-read": "wants to read", + "reading": "started reading", + "read": "finished reading", }[shelf.identifier] except KeyError: # it's a non-standard shelf, don't worry about it return - status = create_generated_note( - user, - message, - mention_books=[book], - privacy=privacy - ) + status = create_generated_note(user, message, mention_books=[book], privacy=privacy) status.save() def is_blocked(viewer, user): - ''' is this viewer blocked by the user? ''' + """ is this viewer blocked by the user? """ if viewer.is_authenticated and viewer in user.blocks.all(): return True return False diff --git a/bookwyrm/views/import_data.py b/bookwyrm/views/import_data.py index cf33163ab..8f9ea27fd 100644 --- a/bookwyrm/views/import_data.py +++ b/bookwyrm/views/import_data.py @@ -1,4 +1,4 @@ -''' import books from another app ''' +""" import books from another app """ from io import TextIOWrapper from django.contrib.auth.decorators import login_required @@ -13,27 +13,33 @@ from bookwyrm import forms, goodreads_import, librarything_import, models from bookwyrm.tasks import app # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Import(View): - ''' import view ''' + """ import view """ + def get(self, request): - ''' load import page ''' - return TemplateResponse(request, 'import.html', { - 'import_form': forms.ImportForm(), - 'jobs': models.ImportJob. - objects.filter(user=request.user).order_by('-created_date'), - }) + """ load import page """ + return TemplateResponse( + request, + "import.html", + { + "import_form": forms.ImportForm(), + "jobs": models.ImportJob.objects.filter(user=request.user).order_by( + "-created_date" + ), + }, + ) def post(self, request): - ''' ingest a goodreads csv ''' + """ ingest a goodreads csv """ form = forms.ImportForm(request.POST, request.FILES) if form.is_valid(): - include_reviews = request.POST.get('include_reviews') == 'on' - privacy = request.POST.get('privacy') - source = request.POST.get('source') + include_reviews = request.POST.get("include_reviews") == "on" + privacy = request.POST.get("privacy") + source = request.POST.get("source") importer = None - if source == 'LibraryThing': + if source == "LibraryThing": importer = librarything_import.LibrarythingImporter() else: # Default : GoodReads @@ -43,44 +49,44 @@ class Import(View): job = importer.create_job( request.user, TextIOWrapper( - request.FILES['csv_file'], - encoding=importer.encoding), + request.FILES["csv_file"], encoding=importer.encoding + ), include_reviews, privacy, ) except (UnicodeDecodeError, ValueError): - return HttpResponseBadRequest('Not a valid csv file') + return HttpResponseBadRequest("Not a valid csv file") importer.start_import(job) - return redirect('/import/%d' % job.id) + return redirect("/import/%d" % job.id) return HttpResponseBadRequest() -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class ImportStatus(View): - ''' status of an existing import ''' + """ status of an existing import """ + def get(self, request, job_id): - ''' status of an import job ''' + """ status of an import job """ job = models.ImportJob.objects.get(id=job_id) if job.user != request.user: raise PermissionDenied task = app.AsyncResult(job.task_id) - items = job.items.order_by('index').all() + items = job.items.order_by("index").all() failed_items = [i for i in items if i.fail_reason] items = [i for i in items if not i.fail_reason] - return TemplateResponse(request, 'import_status.html', { - 'job': job, - 'items': items, - 'failed_items': failed_items, - 'task': task - }) + return TemplateResponse( + request, + "import_status.html", + {"job": job, "items": items, "failed_items": failed_items, "task": task}, + ) def post(self, request, job_id): - ''' retry lines from an import ''' + """ retry lines from an import """ job = get_object_or_404(models.ImportJob, id=job_id) items = [] - for item in request.POST.getlist('import_item'): + for item in request.POST.getlist("import_item"): items.append(get_object_or_404(models.ImportItem, id=item)) job = goodreads_import.create_retry_job( @@ -89,4 +95,4 @@ class ImportStatus(View): items, ) goodreads_import.start_import(job) - return redirect('/import/%d' % job.id) + return redirect("/import/%d" % job.id) diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index 46385093c..34bd2e1cc 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -1,4 +1,4 @@ -''' incoming activities ''' +""" incoming activities """ import json from urllib.parse import urldefrag @@ -14,12 +14,13 @@ from bookwyrm.tasks import app from bookwyrm.signatures import Signature -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") # pylint: disable=no-self-use class Inbox(View): - ''' requests sent by outside servers''' + """ requests sent by outside servers""" + def post(self, request, username=None): - ''' only works as POST request ''' + """ only works as POST request """ # make sure the user's inbox even exists if username: try: @@ -33,14 +34,16 @@ class Inbox(View): except json.decoder.JSONDecodeError: return HttpResponseBadRequest() - if not 'object' in activity_json or \ - not 'type' in activity_json or \ - not activity_json['type'] in activitypub.activity_objects: + if ( + not "object" in activity_json + or not "type" in activity_json + or not activity_json["type"] in activitypub.activity_objects + ): return HttpResponseNotFound() # verify the signature if not has_valid_signature(request, activity_json): - if activity_json['type'] == 'Delete': + if activity_json["type"] == "Delete": # Pretend that unauth'd deletes succeed. Auth may be failing # because the resource or owner of the resource might have # been deleted. @@ -53,7 +56,7 @@ class Inbox(View): @app.task def activity_task(activity_json): - ''' do something with this json we think is legit ''' + """ do something with this json we think is legit """ # lets see if the activitypub module can make sense of this json try: activity = activitypub.parse(activity_json) @@ -70,16 +73,15 @@ def activity_task(activity_json): def has_valid_signature(request, activity): - ''' verify incoming signature ''' + """ verify incoming signature """ try: signature = Signature.parse(request) key_actor = urldefrag(signature.key_id).url - if key_actor != activity.get('actor'): + if key_actor != activity.get("actor"): raise ValueError("Wrong actor created signature.") - remote_user = activitypub.resolve_remote_id( - key_actor, model=models.User) + remote_user = activitypub.resolve_remote_id(key_actor, model=models.User) if not remote_user: return False @@ -91,7 +93,7 @@ def has_valid_signature(request, activity): remote_user.remote_id, model=models.User, refresh=True ) if remote_user.key_pair.public_key == old_key: - raise # Key unchanged. + raise # Key unchanged. signature.verify(remote_user.key_pair.public_key, request) except (ValueError, requests.exceptions.HTTPError): return False diff --git a/bookwyrm/views/interaction.py b/bookwyrm/views/interaction.py index a7fcc231f..e337f2ef6 100644 --- a/bookwyrm/views/interaction.py +++ b/bookwyrm/views/interaction.py @@ -1,4 +1,4 @@ -''' boosts and favs ''' +""" boosts and favs """ from django.db import IntegrityError from django.contrib.auth.decorators import login_required from django.http import HttpResponseBadRequest, HttpResponseNotFound @@ -10,75 +10,74 @@ from bookwyrm import models # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Favorite(View): - ''' like a status ''' + """ like a status """ + def post(self, request, status_id): - ''' create a like ''' + """ create a like """ status = models.Status.objects.get(id=status_id) try: - models.Favorite.objects.create( - status=status, - user=request.user - ) + models.Favorite.objects.create(status=status, user=request.user) except IntegrityError: # you already fav'ed that return HttpResponseBadRequest() - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Unfavorite(View): - ''' take back a fav ''' + """ take back a fav """ + def post(self, request, status_id): - ''' unlike a status ''' + """ unlike a status """ status = models.Status.objects.get(id=status_id) try: - favorite = models.Favorite.objects.get( - status=status, - user=request.user - ) + favorite = models.Favorite.objects.get(status=status, user=request.user) except models.Favorite.DoesNotExist: # can't find that status, idk return HttpResponseNotFound() favorite.delete() - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Boost(View): - ''' boost a status ''' + """ boost a status """ + def post(self, request, status_id): - ''' boost a status ''' + """ boost a status """ status = models.Status.objects.get(id=status_id) # is it boostable? if not status.boostable: return HttpResponseBadRequest() if models.Boost.objects.filter( - boosted_status=status, user=request.user).exists(): + boosted_status=status, user=request.user + ).exists(): # you already boosted that. - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) models.Boost.objects.create( boosted_status=status, privacy=status.privacy, user=request.user, ) - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Unboost(View): - ''' boost a status ''' + """ boost a status """ + def post(self, request, status_id): - ''' boost a status ''' + """ boost a status """ status = models.Status.objects.get(id=status_id) boost = models.Boost.objects.filter( boosted_status=status, user=request.user ).first() boost.delete() - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) diff --git a/bookwyrm/views/invite.py b/bookwyrm/views/invite.py index 750a5c2b9..1f1ccaf11 100644 --- a/bookwyrm/views/invite.py +++ b/bookwyrm/views/invite.py @@ -1,4 +1,4 @@ -''' invites when registration is closed ''' +""" invites when registration is closed """ from django.contrib.auth.decorators import login_required, permission_required from django.core.paginator import Paginator from django.http import HttpResponseBadRequest @@ -12,31 +12,36 @@ from bookwyrm.settings import PAGE_LENGTH # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") @method_decorator( - permission_required('bookwyrm.create_invites', raise_exception=True), - name='dispatch') + permission_required("bookwyrm.create_invites", raise_exception=True), + name="dispatch", +) class ManageInvites(View): - ''' create invites ''' + """ create invites """ + def get(self, request): - ''' invite management page ''' + """ invite management page """ try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 - paginated = Paginator(models.SiteInvite.objects.filter( - user=request.user - ).order_by('-created_date'), PAGE_LENGTH) + paginated = Paginator( + models.SiteInvite.objects.filter(user=request.user).order_by( + "-created_date" + ), + PAGE_LENGTH, + ) data = { - 'invites': paginated.page(page), - 'form': forms.CreateInviteForm(), + "invites": paginated.page(page), + "form": forms.CreateInviteForm(), } - return TemplateResponse(request, 'settings/manage_invites.html', data) + return TemplateResponse(request, "settings/manage_invites.html", data) def post(self, request): - ''' creates an invite database entry ''' + """ creates an invite database entry """ form = forms.CreateInviteForm(request.POST) if not form.is_valid(): return HttpResponseBadRequest("ERRORS : %s" % (form.errors,)) @@ -45,29 +50,30 @@ class ManageInvites(View): invite.user = request.user invite.save() - paginated = Paginator(models.SiteInvite.objects.filter( - user=request.user - ).order_by('-created_date'), PAGE_LENGTH) - data = { - 'invites': paginated.page(1), - 'form': form - } - return TemplateResponse(request, 'settings/manage_invites.html', data) + paginated = Paginator( + models.SiteInvite.objects.filter(user=request.user).order_by( + "-created_date" + ), + PAGE_LENGTH, + ) + data = {"invites": paginated.page(1), "form": form} + return TemplateResponse(request, "settings/manage_invites.html", data) class Invite(View): - ''' use an invite to register ''' + """ use an invite to register """ + def get(self, request, code): - ''' endpoint for using an invites ''' + """ endpoint for using an invites """ if request.user.is_authenticated: - return redirect('/') + return redirect("/") invite = get_object_or_404(models.SiteInvite, code=code) data = { - 'register_form': forms.RegisterForm(), - 'invite': invite, - 'valid': invite.valid() if invite else True, + "register_form": forms.RegisterForm(), + "invite": invite, + "valid": invite.valid() if invite else True, } - return TemplateResponse(request, 'invite.html', data) + return TemplateResponse(request, "invite.html", data) # post handling is in views.authentication.Register diff --git a/bookwyrm/views/isbn.py b/bookwyrm/views/isbn.py index e5539ba3a..b7ba02dd8 100644 --- a/bookwyrm/views/isbn.py +++ b/bookwyrm/views/isbn.py @@ -1,4 +1,4 @@ -''' isbn search view ''' +""" isbn search view """ from django.http import HttpResponseNotFound from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect @@ -13,17 +13,18 @@ from .helpers import is_api_request # pylint: disable= no-self-use class Isbn(View): - ''' search a book by isbn ''' + """ search a book by isbn """ + def get(self, request, isbn): - ''' info about a book ''' + """ info about a book """ book_results = connector_manager.isbn_local_search(isbn) if is_api_request(request): return JsonResponse([r.json() for r in book_results], safe=False) data = { - 'title': 'ISBN Search Results', - 'results': book_results, - 'query': isbn, + "title": "ISBN Search Results", + "results": book_results, + "query": isbn, } - return TemplateResponse(request, 'isbn_search_results.html', data) + return TemplateResponse(request, "isbn_search_results.html", data) diff --git a/bookwyrm/views/landing.py b/bookwyrm/views/landing.py index 94b27b8fc..2c4a51478 100644 --- a/bookwyrm/views/landing.py +++ b/bookwyrm/views/landing.py @@ -1,4 +1,4 @@ -''' non-interactive pages ''' +""" non-interactive pages """ from django.db.models import Max from django.template.response import TemplateResponse from django.views import View @@ -9,38 +9,44 @@ from .feed import Feed # pylint: disable= no-self-use class About(View): - ''' create invites ''' + """ create invites """ + def get(self, request): - ''' more information about the instance ''' - return TemplateResponse(request, 'discover/about.html') + """ more information about the instance """ + return TemplateResponse(request, "discover/about.html") + class Home(View): - ''' discover page or home feed depending on auth ''' + """ discover page or home feed depending on auth """ + def get(self, request): - ''' this is the same as the feed on the home tab ''' + """ this is the same as the feed on the home tab """ if request.user.is_authenticated: feed_view = Feed.as_view() - return feed_view(request, 'home') + return feed_view(request, "home") discover_view = Discover.as_view() return discover_view(request) + class Discover(View): - ''' preview of recently reviewed books ''' + """ preview of recently reviewed books """ + def get(self, request): - ''' tiled book activity page ''' - books = models.Edition.objects.filter( - review__published_date__isnull=False, - review__deleted=False, - review__user__local=True, - review__privacy__in=['public', 'unlisted'], - ).exclude( - cover__exact='' - ).annotate( - Max('review__published_date') - ).order_by('-review__published_date__max')[:6] + """ tiled book activity page """ + books = ( + models.Edition.objects.filter( + review__published_date__isnull=False, + review__deleted=False, + review__user__local=True, + review__privacy__in=["public", "unlisted"], + ) + .exclude(cover__exact="") + .annotate(Max("review__published_date")) + .order_by("-review__published_date__max")[:6] + ) data = { - 'register_form': forms.RegisterForm(), - 'books': list(set(books)), + "register_form": forms.RegisterForm(), + "books": list(set(books)), } - return TemplateResponse(request, 'discover/discover.html', data) + return TemplateResponse(request, "discover/discover.html", data) diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index b1741b117..ba3200d1e 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -1,4 +1,4 @@ -''' book list views''' +""" book list views""" from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db import IntegrityError @@ -18,51 +18,57 @@ from .helpers import get_user_from_username # pylint: disable=no-self-use class Lists(View): - ''' book list page ''' + """ book list page """ + def get(self, request): - ''' display a book list ''' + """ display a book list """ try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 user = request.user if request.user.is_authenticated else None # hide lists with no approved books - lists = models.List.objects.filter( - ~Q(user=user), - ).annotate( - item_count=Count('listitem', filter=Q(listitem__approved=True)) - ).filter( - item_count__gt=0 - ).distinct().all() + lists = ( + models.List.objects.filter( + ~Q(user=user), + ) + .annotate(item_count=Count("listitem", filter=Q(listitem__approved=True))) + .filter(item_count__gt=0) + .distinct() + .all() + ) lists = privacy_filter( - request.user, lists, privacy_levels=['public', 'followers']) + request.user, lists, privacy_levels=["public", "followers"] + ) paginated = Paginator(lists, 12) data = { - 'lists': paginated.page(page), - 'list_form': forms.ListForm(), - 'path': '/list', + "lists": paginated.page(page), + "list_form": forms.ListForm(), + "path": "/list", } - return TemplateResponse(request, 'lists/lists.html', data) + return TemplateResponse(request, "lists/lists.html", data) - @method_decorator(login_required, name='dispatch') + @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request): - ''' create a book_list ''' + """ create a book_list """ form = forms.ListForm(request.POST) if not form.is_valid(): - return redirect('lists') + return redirect("lists") book_list = form.save() return redirect(book_list.local_path) + class UserLists(View): - ''' a user's book list page ''' + """ a user's book list page """ + def get(self, request, username): - ''' display a book list ''' + """ display a book list """ try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 user = get_user_from_username(request.user, username) @@ -71,19 +77,20 @@ class UserLists(View): paginated = Paginator(lists, 12) data = { - 'user': user, - 'is_self': request.user.id == user.id, - 'lists': paginated.page(page), - 'list_form': forms.ListForm(), - 'path': user.local_path + '/lists', + "user": user, + "is_self": request.user.id == user.id, + "lists": paginated.page(page), + "list_form": forms.ListForm(), + "path": user.local_path + "/lists", } - return TemplateResponse(request, 'user/lists.html', data) + return TemplateResponse(request, "user/lists.html", data) class List(View): - ''' book list page ''' + """ book list page """ + def get(self, request, list_id): - ''' display a book list ''' + """ display a book list """ book_list = get_object_or_404(models.List, id=list_id) if not object_visible_to_user(request.user, book_list): return HttpResponseNotFound() @@ -91,7 +98,7 @@ class List(View): if is_api_request(request): return ActivitypubResponse(book_list.to_activity(**request.GET)) - query = request.GET.get('q') + query = request.GET.get("q") suggestions = None if query and request.user.is_authenticated: # search for books @@ -104,89 +111,85 @@ class List(View): suggestions = [s.book for s in suggestions[:5]] if len(suggestions) < 5: suggestions += [ - s.default_edition for s in \ - models.Work.objects.filter( - ~Q(editions__in=book_list.books.all()), - ).order_by('-updated_date') - ][:5 - len(suggestions)] - + s.default_edition + for s in models.Work.objects.filter( + ~Q(editions__in=book_list.books.all()), + ).order_by("-updated_date") + ][: 5 - len(suggestions)] data = { - 'list': book_list, - 'items': book_list.listitem_set.filter(approved=True), - 'pending_count': book_list.listitem_set.filter( - approved=False).count(), - 'suggested_books': suggestions, - 'list_form': forms.ListForm(instance=book_list), - 'query': query or '' + "list": book_list, + "items": book_list.listitem_set.filter(approved=True), + "pending_count": book_list.listitem_set.filter(approved=False).count(), + "suggested_books": suggestions, + "list_form": forms.ListForm(instance=book_list), + "query": query or "", } - return TemplateResponse(request, 'lists/list.html', data) + return TemplateResponse(request, "lists/list.html", data) - - @method_decorator(login_required, name='dispatch') + @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, list_id): - ''' edit a list ''' + """ edit a list """ book_list = get_object_or_404(models.List, id=list_id) form = forms.ListForm(request.POST, instance=book_list) if not form.is_valid(): - return redirect('list', book_list.id) + return redirect("list", book_list.id) book_list = form.save() return redirect(book_list.local_path) class Curate(View): - ''' approve or discard list suggestsions ''' - @method_decorator(login_required, name='dispatch') + """ approve or discard list suggestsions """ + + @method_decorator(login_required, name="dispatch") def get(self, request, list_id): - ''' display a pending list ''' + """ display a pending list """ book_list = get_object_or_404(models.List, id=list_id) if not book_list.user == request.user: # only the creater can curate the list return HttpResponseNotFound() data = { - 'list': book_list, - 'pending': book_list.listitem_set.filter(approved=False), - 'list_form': forms.ListForm(instance=book_list), + "list": book_list, + "pending": book_list.listitem_set.filter(approved=False), + "list_form": forms.ListForm(instance=book_list), } - return TemplateResponse(request, 'lists/curate.html', data) + return TemplateResponse(request, "lists/curate.html", data) - - @method_decorator(login_required, name='dispatch') + @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, list_id): - ''' edit a book_list ''' + """ edit a book_list """ book_list = get_object_or_404(models.List, id=list_id) - suggestion = get_object_or_404( - models.ListItem, id=request.POST.get('item')) - approved = request.POST.get('approved') == 'true' + suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item")) + approved = request.POST.get("approved") == "true" if approved: suggestion.approved = True suggestion.save() else: suggestion.delete() - return redirect('list-curate', book_list.id) + return redirect("list-curate", book_list.id) @require_POST def add_book(request, list_id): - ''' put a book on a list ''' + """ put a book on a list """ book_list = get_object_or_404(models.List, id=list_id) if not object_visible_to_user(request.user, book_list): return HttpResponseNotFound() - book = get_object_or_404(models.Edition, id=request.POST.get('book')) + book = get_object_or_404(models.Edition, id=request.POST.get("book")) # do you have permission to add to the list? try: - if request.user == book_list.user or book_list.curation == 'open': + if request.user == book_list.user or book_list.curation == "open": # go ahead and add it models.ListItem.objects.create( book=book, book_list=book_list, user=request.user, ) - elif book_list.curation == 'curated': + elif book_list.curation == "curated": # make a pending entry models.ListItem.objects.create( approved=False, @@ -201,17 +204,17 @@ def add_book(request, list_id): # if the book is already on the list, don't flip out pass - return redirect('list', list_id) + return redirect("list", list_id) @require_POST def remove_book(request, list_id): - ''' put a book on a list ''' + """ put a book on a list """ book_list = get_object_or_404(models.List, id=list_id) - item = get_object_or_404(models.ListItem, id=request.POST.get('item')) + item = get_object_or_404(models.ListItem, id=request.POST.get("item")) if not book_list.user == request.user and not item.user == request.user: return HttpResponseNotFound() item.delete() - return redirect('list', list_id) + return redirect("list", list_id) diff --git a/bookwyrm/views/notifications.py b/bookwyrm/views/notifications.py index 684154e4c..7a62ec01e 100644 --- a/bookwyrm/views/notifications.py +++ b/bookwyrm/views/notifications.py @@ -1,4 +1,4 @@ -''' non-interactive pages ''' +""" non-interactive pages """ from django.contrib.auth.decorators import login_required from django.template.response import TemplateResponse from django.utils.decorators import method_decorator @@ -7,22 +7,22 @@ from django.views import View # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Notifications(View): - ''' notifications view ''' + """ notifications view """ + def get(self, request): - ''' people are interacting with you, get hyped ''' - notifications = request.user.notification_set.all() \ - .order_by('-created_date') + """ people are interacting with you, get hyped """ + notifications = request.user.notification_set.all().order_by("-created_date") unread = [n.id for n in notifications.filter(read=False)] data = { - 'notifications': notifications, - 'unread': unread, + "notifications": notifications, + "unread": unread, } notifications.update(read=True) - return TemplateResponse(request, 'notifications.html', data) + return TemplateResponse(request, "notifications.html", data) def post(self, request): - ''' permanently delete notification for user ''' + """ permanently delete notification for user """ request.user.notification_set.filter(read=True).delete() - return redirect('/notifications') + return redirect("/notifications") diff --git a/bookwyrm/views/outbox.py b/bookwyrm/views/outbox.py index 5df9d1999..ec6f5cd39 100644 --- a/bookwyrm/views/outbox.py +++ b/bookwyrm/views/outbox.py @@ -1,4 +1,4 @@ -''' the good stuff! the books! ''' +""" the good stuff! the books! """ from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.views import View @@ -9,11 +9,12 @@ from .helpers import is_bookwyrm_request # pylint: disable= no-self-use class Outbox(View): - ''' outbox ''' + """ outbox """ + def get(self, request, username): - ''' outbox for the requested user ''' + """ outbox for the requested user """ user = get_object_or_404(models.User, localname=username) - filter_type = request.GET.get('type') + filter_type = request.GET.get("type") if filter_type not in models.status_models: filter_type = None @@ -23,5 +24,5 @@ class Outbox(View): filter_type=filter_type, pure=not is_bookwyrm_request(request) ), - encoder=activitypub.ActivityEncoder + encoder=activitypub.ActivityEncoder, ) diff --git a/bookwyrm/views/password.py b/bookwyrm/views/password.py index 792da2d78..e853d16bf 100644 --- a/bookwyrm/views/password.py +++ b/bookwyrm/views/password.py @@ -1,4 +1,4 @@ -''' class views for password management ''' +""" class views for password management """ from django.contrib.auth import login from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied @@ -13,21 +13,22 @@ from bookwyrm.emailing import password_reset_email # pylint: disable= no-self-use class PasswordResetRequest(View): - ''' forgot password flow ''' + """ forgot password flow """ + def get(self, request): - ''' password reset page ''' + """ password reset page """ return TemplateResponse( request, - 'password_reset_request.html', + "password_reset_request.html", ) def post(self, request): - ''' create a password reset token ''' - email = request.POST.get('email') + """ create a password reset token """ + email = request.POST.get("email") try: user = models.User.objects.get(email=email) except models.User.DoesNotExist: - return redirect('/password-reset') + return redirect("/password-reset") # remove any existing password reset cods for this user models.PasswordReset.objects.filter(user=user).all().delete() @@ -35,16 +36,17 @@ class PasswordResetRequest(View): # create a new reset code code = models.PasswordReset.objects.create(user=user) password_reset_email(code) - data = {'message': 'Password reset link sent to %s' % email} - return TemplateResponse(request, 'password_reset_request.html', data) + data = {"message": "Password reset link sent to %s" % email} + return TemplateResponse(request, "password_reset_request.html", data) class PasswordReset(View): - ''' set new password ''' + """ set new password """ + def get(self, request, code): - ''' endpoint for sending invites ''' + """ endpoint for sending invites """ if request.user.is_authenticated: - return redirect('/') + return redirect("/") try: reset_code = models.PasswordReset.objects.get(code=code) if not reset_code.valid(): @@ -52,50 +54,48 @@ class PasswordReset(View): except models.PasswordReset.DoesNotExist: raise PermissionDenied - return TemplateResponse(request, 'password_reset.html') + return TemplateResponse(request, "password_reset.html") def post(self, request, code): - ''' allow a user to change their password through an emailed token ''' + """ allow a user to change their password through an emailed token """ try: - reset_code = models.PasswordReset.objects.get( - code=code - ) + reset_code = models.PasswordReset.objects.get(code=code) except models.PasswordReset.DoesNotExist: - data = {'errors': ['Invalid password reset link']} - return TemplateResponse(request, 'password_reset.html', data) + data = {"errors": ["Invalid password reset link"]} + return TemplateResponse(request, "password_reset.html", data) user = reset_code.user - new_password = request.POST.get('password') - confirm_password = request.POST.get('confirm-password') + new_password = request.POST.get("password") + confirm_password = request.POST.get("confirm-password") if new_password != confirm_password: - data = {'errors': ['Passwords do not match']} - return TemplateResponse(request, 'password_reset.html', data) + data = {"errors": ["Passwords do not match"]} + return TemplateResponse(request, "password_reset.html", data) user.set_password(new_password) user.save(broadcast=False) login(request, user) reset_code.delete() - return redirect('/') + return redirect("/") -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class ChangePassword(View): - ''' change password as logged in user ''' + """ change password as logged in user """ + def get(self, request): - ''' change password page ''' - data = {'user': request.user} - return TemplateResponse( - request, 'preferences/change_password.html', data) + """ change password page """ + data = {"user": request.user} + return TemplateResponse(request, "preferences/change_password.html", data) def post(self, request): - ''' allow a user to change their password ''' - new_password = request.POST.get('password') - confirm_password = request.POST.get('confirm-password') + """ allow a user to change their password """ + new_password = request.POST.get("password") + confirm_password = request.POST.get("confirm-password") if new_password != confirm_password: - return redirect('preferences/password') + return redirect("preferences/password") request.user.set_password(new_password) request.user.save(broadcast=False) diff --git a/bookwyrm/views/reading.py b/bookwyrm/views/reading.py index 6b2bf242e..fb63f2a3e 100644 --- a/bookwyrm/views/reading.py +++ b/bookwyrm/views/reading.py @@ -1,4 +1,4 @@ -''' the good stuff! the books! ''' +""" the good stuff! the books! """ import dateutil.parser from dateutil.parser import ParserError @@ -17,12 +17,9 @@ from .shelf import handle_unshelve @login_required @require_POST def start_reading(request, book_id): - ''' begin reading a book ''' + """ begin reading a book """ book = get_edition(book_id) - shelf = models.Shelf.objects.filter( - identifier='reading', - user=request.user - ).first() + shelf = models.Shelf.objects.filter(identifier="reading", user=request.user).first() # create a readthrough readthrough = update_readthrough(request, book=book) @@ -33,36 +30,29 @@ def start_reading(request, book_id): readthrough.create_update() # shelve the book - if request.POST.get('reshelve', True): + if request.POST.get("reshelve", True): try: - current_shelf = models.Shelf.objects.get( - user=request.user, - edition=book - ) + current_shelf = models.Shelf.objects.get(user=request.user, edition=book) handle_unshelve(request.user, book, current_shelf) except models.Shelf.DoesNotExist: # this just means it isn't currently on the user's shelves pass - models.ShelfBook.objects.create( - book=book, shelf=shelf, user=request.user) + models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user) # post about it (if you want) - if request.POST.get('post-status'): - privacy = request.POST.get('privacy') + if request.POST.get("post-status"): + privacy = request.POST.get("privacy") handle_reading_status(request.user, shelf, book, privacy) - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) @login_required @require_POST def finish_reading(request, book_id): - ''' a user completed a book, yay ''' + """ a user completed a book, yay """ book = get_edition(book_id) - shelf = models.Shelf.objects.filter( - identifier='read', - user=request.user - ).first() + shelf = models.Shelf.objects.filter(identifier="read", user=request.user).first() # update or create a readthrough readthrough = update_readthrough(request, book=book) @@ -70,31 +60,27 @@ def finish_reading(request, book_id): readthrough.save() # shelve the book - if request.POST.get('reshelve', True): + if request.POST.get("reshelve", True): try: - current_shelf = models.Shelf.objects.get( - user=request.user, - edition=book - ) + current_shelf = models.Shelf.objects.get(user=request.user, edition=book) handle_unshelve(request.user, book, current_shelf) except models.Shelf.DoesNotExist: # this just means it isn't currently on the user's shelves pass - models.ShelfBook.objects.create( - book=book, shelf=shelf, user=request.user) + models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user) # post about it (if you want) - if request.POST.get('post-status'): - privacy = request.POST.get('privacy') + if request.POST.get("post-status"): + privacy = request.POST.get("privacy") handle_reading_status(request.user, shelf, book, privacy) - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) @login_required @require_POST def edit_readthrough(request): - ''' can't use the form because the dates are too finnicky ''' + """ can't use the form because the dates are too finnicky """ readthrough = update_readthrough(request, create=False) if not readthrough: return HttpResponseNotFound() @@ -108,40 +94,39 @@ def edit_readthrough(request): # use default now for date field readthrough.create_update() - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) @login_required @require_POST def delete_readthrough(request): - ''' remove a readthrough ''' - readthrough = get_object_or_404( - models.ReadThrough, id=request.POST.get('id')) + """ remove a readthrough """ + readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id")) # don't let people edit other people's data if request.user != readthrough.user: return HttpResponseBadRequest() readthrough.delete() - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) @login_required @require_POST def create_readthrough(request): - ''' can't use the form because the dates are too finnicky ''' - book = get_object_or_404(models.Edition, id=request.POST.get('book')) + """ can't use the form because the dates are too finnicky """ + book = get_object_or_404(models.Edition, id=request.POST.get("book")) readthrough = update_readthrough(request, create=True, book=book) if not readthrough: return redirect(book.local_path) readthrough.save() - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) def update_readthrough(request, book=None, create=True): - ''' updates but does not save dates on a readthrough ''' + """ updates but does not save dates on a readthrough """ try: - read_id = request.POST.get('id') + read_id = request.POST.get("id") if not read_id: raise models.ReadThrough.DoesNotExist readthrough = models.ReadThrough.objects.get(id=read_id) @@ -153,7 +138,7 @@ def update_readthrough(request, book=None, create=True): book=book, ) - start_date = request.POST.get('start_date') + start_date = request.POST.get("start_date") if start_date: try: start_date = timezone.make_aware(dateutil.parser.parse(start_date)) @@ -161,16 +146,15 @@ def update_readthrough(request, book=None, create=True): except ParserError: pass - finish_date = request.POST.get('finish_date') + finish_date = request.POST.get("finish_date") if finish_date: try: - finish_date = timezone.make_aware( - dateutil.parser.parse(finish_date)) + finish_date = timezone.make_aware(dateutil.parser.parse(finish_date)) readthrough.finish_date = finish_date except ParserError: pass - progress = request.POST.get('progress') + progress = request.POST.get("progress") if progress: try: progress = int(progress) @@ -178,7 +162,7 @@ def update_readthrough(request, book=None, create=True): except ValueError: pass - progress_mode = request.POST.get('progress_mode') + progress_mode = request.POST.get("progress_mode") if progress_mode: try: progress_mode = models.ProgressMode(progress_mode) @@ -191,15 +175,16 @@ def update_readthrough(request, book=None, create=True): return readthrough + @login_required @require_POST def delete_progressupdate(request): - ''' remove a progress update ''' - update = get_object_or_404(models.ProgressUpdate, id=request.POST.get('id')) + """ remove a progress update """ + update = get_object_or_404(models.ProgressUpdate, id=request.POST.get("id")) # don't let people edit other people's data if request.user != update.user: return HttpResponseBadRequest() update.delete() - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) diff --git a/bookwyrm/views/rss_feed.py b/bookwyrm/views/rss_feed.py index d24b636ea..57821af4e 100644 --- a/bookwyrm/views/rss_feed.py +++ b/bookwyrm/views/rss_feed.py @@ -1,38 +1,35 @@ -''' serialize user's posts in rss feed ''' +""" serialize user's posts in rss feed """ from django.contrib.syndication.views import Feed from .helpers import get_activity_feed, get_user_from_username # pylint: disable=no-self-use, unused-argument class RssFeed(Feed): - ''' serialize user's posts in rss feed ''' - description_template = 'snippets/rss_content.html' - title_template = 'snippets/rss_title.html' + """ serialize user's posts in rss feed """ + + description_template = "snippets/rss_content.html" + title_template = "snippets/rss_title.html" def get_object(self, request, username): - ''' the user who's posts get serialized ''' + """ the user who's posts get serialized """ return get_user_from_username(request.user, username) - def link(self, obj): - ''' link to the user's profile ''' + """ link to the user's profile """ return obj.local_path - def title(self, obj): - ''' title of the rss feed entry ''' - return f'Status updates from {obj.display_name}' - + """ title of the rss feed entry """ + return f"Status updates from {obj.display_name}" def items(self, obj): - ''' the user's activity feed ''' + """ the user's activity feed """ return get_activity_feed( obj, - privacy=['public', 'unlisted'], - queryset=obj.status_set.select_subclasses() + privacy=["public", "unlisted"], + queryset=obj.status_set.select_subclasses(), ) - def item_link(self, item): - ''' link to the status ''' + """ link to the status """ return item.local_path diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index b44c49f81..80969d31b 100644 --- a/bookwyrm/views/search.py +++ b/bookwyrm/views/search.py @@ -1,4 +1,4 @@ -''' search views''' +""" search views""" import re from django.contrib.postgres.search import TrigramSimilarity @@ -16,51 +16,63 @@ from .helpers import handle_remote_webfinger # pylint: disable= no-self-use class Search(View): - ''' search users or books ''' + """ search users or books """ + def get(self, request): - ''' that search bar up top ''' - query = request.GET.get('q') - min_confidence = request.GET.get('min_confidence', 0.1) + """ that search bar up top """ + query = request.GET.get("q") + min_confidence = request.GET.get("min_confidence", 0.1) if is_api_request(request): # only return local book results via json so we don't cascade book_results = connector_manager.local_search( - query, min_confidence=min_confidence) + query, min_confidence=min_confidence + ) return JsonResponse([r.json() for r in book_results], safe=False) # use webfinger for mastodon style account@domain.com username - if re.match(r'\B%s' % regex.full_username, query): + if re.match(r"\B%s" % regex.full_username, query): handle_remote_webfinger(query) # do a user search - user_results = models.User.viewer_aware_objects(request.user).annotate( - similarity=Greatest( - TrigramSimilarity('username', query), - TrigramSimilarity('localname', query), + user_results = ( + models.User.viewer_aware_objects(request.user) + .annotate( + similarity=Greatest( + TrigramSimilarity("username", query), + TrigramSimilarity("localname", query), + ) ) - ).filter( - similarity__gt=0.5, - ).order_by('-similarity')[:10] + .filter( + similarity__gt=0.5, + ) + .order_by("-similarity")[:10] + ) # any relevent lists? - list_results = privacy_filter( - request.user, models.List.objects, - privacy_levels=['public', 'followers'] - ).annotate( - similarity=Greatest( - TrigramSimilarity('name', query), - TrigramSimilarity('description', query), + list_results = ( + privacy_filter( + request.user, + models.List.objects, + privacy_levels=["public", "followers"], ) - ).filter( - similarity__gt=0.1, - ).order_by('-similarity')[:10] + .annotate( + similarity=Greatest( + TrigramSimilarity("name", query), + TrigramSimilarity("description", query), + ) + ) + .filter( + similarity__gt=0.1, + ) + .order_by("-similarity")[:10] + ) - book_results = connector_manager.search( - query, min_confidence=min_confidence) + book_results = connector_manager.search(query, min_confidence=min_confidence) data = { - 'book_results': book_results, - 'user_results': user_results, - 'list_results': list_results, - 'query': query, + "book_results": book_results, + "user_results": user_results, + "list_results": list_results, + "query": query, } - return TemplateResponse(request, 'search_results.html', data) + return TemplateResponse(request, "search_results.html", data) diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index 867c7d91c..6256eac7f 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -1,4 +1,4 @@ -''' shelf views''' +""" shelf views""" from django.contrib.auth.decorators import login_required from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect @@ -15,9 +15,10 @@ from .helpers import handle_reading_status # pylint: disable= no-self-use class Shelf(View): - ''' shelf page ''' + """ shelf page """ + def get(self, request, username, shelf_identifier): - ''' display a shelf ''' + """ display a shelf """ try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -34,38 +35,40 @@ class Shelf(View): if not is_self: follower = user.followers.filter(id=request.user.id).exists() # make sure the user has permission to view the shelf - if shelf.privacy == 'direct' or \ - (shelf.privacy == 'followers' and not follower): + if shelf.privacy == "direct" or ( + shelf.privacy == "followers" and not follower + ): return HttpResponseNotFound() # only show other shelves that should be visible if follower: - shelves = shelves.filter(privacy__in=['public', 'followers']) + shelves = shelves.filter(privacy__in=["public", "followers"]) else: - shelves = shelves.filter(privacy='public') - + shelves = shelves.filter(privacy="public") if is_api_request(request): return ActivitypubResponse(shelf.to_activity(**request.GET)) - books = models.ShelfBook.objects.filter( - user=user, shelf=shelf - ).order_by('-updated_date').all() + books = ( + models.ShelfBook.objects.filter(user=user, shelf=shelf) + .order_by("-updated_date") + .all() + ) data = { - 'user': user, - 'is_self': is_self, - 'shelves': shelves.all(), - 'shelf': shelf, - 'books': [b.book for b in books], + "user": user, + "is_self": is_self, + "shelves": shelves.all(), + "shelf": shelf, + "books": [b.book for b in books], } - return TemplateResponse(request, 'user/shelf.html', data) + return TemplateResponse(request, "user/shelf.html", data) - @method_decorator(login_required, name='dispatch') + @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, username, shelf_identifier): - ''' edit a shelf ''' + """ edit a shelf """ try: shelf = request.user.shelf_set.get(identifier=shelf_identifier) except models.Shelf.DoesNotExist: @@ -73,7 +76,7 @@ class Shelf(View): if request.user != shelf.user: return HttpResponseBadRequest() - if not shelf.editable and request.POST.get('name') != shelf.name: + if not shelf.editable and request.POST.get("name") != shelf.name: return HttpResponseBadRequest() form = forms.ShelfForm(request.POST, instance=shelf) @@ -84,88 +87,76 @@ class Shelf(View): def user_shelves_page(request, username): - ''' default shelf ''' + """ default shelf """ return Shelf.as_view()(request, username, None) @login_required @require_POST def create_shelf(request): - ''' user generated shelves ''' + """ user generated shelves """ form = forms.ShelfForm(request.POST) if not form.is_valid(): - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) shelf = form.save() - return redirect('/user/%s/shelf/%s' % \ - (request.user.localname, shelf.identifier)) + return redirect("/user/%s/shelf/%s" % (request.user.localname, shelf.identifier)) @login_required @require_POST def delete_shelf(request, shelf_id): - ''' user generated shelves ''' + """ user generated shelves """ shelf = get_object_or_404(models.Shelf, id=shelf_id) if request.user != shelf.user or not shelf.editable: return HttpResponseBadRequest() shelf.delete() - return redirect('/user/%s/shelves' % request.user.localname) + return redirect("/user/%s/shelves" % request.user.localname) @login_required @require_POST def shelve(request): - ''' put a on a user's shelf ''' - book = get_edition(request.POST.get('book')) + """ put a on a user's shelf """ + book = get_edition(request.POST.get("book")) desired_shelf = models.Shelf.objects.filter( - identifier=request.POST.get('shelf'), - user=request.user + identifier=request.POST.get("shelf"), user=request.user ).first() if not desired_shelf: return HttpResponseNotFound() - if request.POST.get('reshelve', True): + if request.POST.get("reshelve", True): try: - current_shelf = models.Shelf.objects.get( - user=request.user, - edition=book - ) + current_shelf = models.Shelf.objects.get(user=request.user, edition=book) handle_unshelve(request.user, book, current_shelf) except models.Shelf.DoesNotExist: # this just means it isn't currently on the user's shelves pass - models.ShelfBook.objects.create( - book=book, shelf=desired_shelf, user=request.user) + models.ShelfBook.objects.create(book=book, shelf=desired_shelf, user=request.user) # post about "want to read" shelves - if desired_shelf.identifier == 'to-read' and \ - request.POST.get('post-status'): - privacy = request.POST.get('privacy') or desired_shelf.privacy - handle_reading_status( - request.user, - desired_shelf, - book, - privacy=privacy - ) + if desired_shelf.identifier == "to-read" and request.POST.get("post-status"): + privacy = request.POST.get("privacy") or desired_shelf.privacy + handle_reading_status(request.user, desired_shelf, book, privacy=privacy) - return redirect('/') + return redirect("/") @login_required @require_POST def unshelve(request): - ''' put a on a user's shelf ''' - book = models.Edition.objects.get(id=request.POST['book']) - current_shelf = models.Shelf.objects.get(id=request.POST['shelf']) + """ put a on a user's shelf """ + book = models.Edition.objects.get(id=request.POST["book"]) + current_shelf = models.Shelf.objects.get(id=request.POST["shelf"]) handle_unshelve(request.user, book, current_shelf) - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) -#pylint: disable=unused-argument +# pylint: disable=unused-argument def handle_unshelve(user, book, shelf): - ''' unshelve a book ''' + """ unshelve a book """ row = models.ShelfBook.objects.get(book=book, shelf=shelf) row.delete() diff --git a/bookwyrm/views/site.py b/bookwyrm/views/site.py index e60354a32..c40a9e760 100644 --- a/bookwyrm/views/site.py +++ b/bookwyrm/views/site.py @@ -1,4 +1,4 @@ -''' manage site settings ''' +""" manage site settings """ from django.contrib.auth.decorators import login_required, permission_required from django.shortcuts import redirect from django.template.response import TemplateResponse @@ -9,26 +9,27 @@ from bookwyrm import forms, models # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") @method_decorator( - permission_required( - 'bookwyrm.edit_instance_settings', raise_exception=True), - name='dispatch') + permission_required("bookwyrm.edit_instance_settings", raise_exception=True), + name="dispatch", +) class Site(View): - ''' manage things like the instance name ''' + """ manage things like the instance name """ + def get(self, request): - ''' edit form ''' + """ edit form """ site = models.SiteSettings.objects.get() - data = {'site_form': forms.SiteForm(instance=site)} - return TemplateResponse(request, 'settings/site.html', data) + data = {"site_form": forms.SiteForm(instance=site)} + return TemplateResponse(request, "settings/site.html", data) def post(self, request): - ''' edit the site settings ''' + """ edit the site settings """ site = models.SiteSettings.objects.get() form = forms.SiteForm(request.POST, instance=site) if not form.is_valid(): - data = {'site_form': form} - return TemplateResponse(request, 'settings/site.html', data) + data = {"site_form": form} + return TemplateResponse(request, "settings/site.html", data) form.save() - return redirect('settings-site') + return redirect("settings-site") diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py index db924ce8b..9a626371c 100644 --- a/bookwyrm/views/status.py +++ b/bookwyrm/views/status.py @@ -1,4 +1,4 @@ -''' what are we here for if not for posting ''' +""" what are we here for if not for posting """ import re from django.contrib.auth.decorators import login_required from django.http import HttpResponseBadRequest @@ -16,19 +16,20 @@ from .helpers import handle_remote_webfinger # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class CreateStatus(View): - ''' the view for *posting* ''' + """ the view for *posting* """ + def post(self, request, status_type): - ''' create status of whatever type ''' + """ create status of whatever type """ status_type = status_type[0].upper() + status_type[1:] try: - form = getattr(forms, '%sForm' % status_type)(request.POST) + form = getattr(forms, "%sForm" % status_type)(request.POST) except AttributeError: return HttpResponseBadRequest() if not form.is_valid(): - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) status = form.save(commit=False) if not status.sensitive and status.content_warning: @@ -44,10 +45,10 @@ class CreateStatus(View): # turn the mention into a link content = re.sub( - r'%s([^@]|$)' % mention_text, - r'%s\g<1>' % \ - (mention_user.remote_id, mention_text), - content) + r"%s([^@]|$)" % mention_text, + r'%s\g<1>' % (mention_user.remote_id, mention_text), + content, + ) # add reply parent to mentions if status.reply_parent: status.mention_users.add(status.reply_parent.user) @@ -59,17 +60,18 @@ class CreateStatus(View): if not isinstance(status, models.GeneratedNote): status.content = to_markdown(content) # do apply formatting to quotes - if hasattr(status, 'quote'): + if hasattr(status, "quote"): status.quote = to_markdown(status.quote) status.save(created=True) - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) class DeleteStatus(View): - ''' tombstone that bad boy ''' + """ tombstone that bad boy """ + def post(self, request, status_id): - ''' delete and tombstone a status ''' + """ delete and tombstone a status """ status = get_object_or_404(models.Status, id=status_id) # don't let people delete other people's statuses @@ -78,16 +80,17 @@ class DeleteStatus(View): # perform deletion delete_status(status) - return redirect(request.headers.get('Referer', '/')) + return redirect(request.headers.get("Referer", "/")) + def find_mentions(content): - ''' detect @mentions in raw status content ''' + """ detect @mentions in raw status content """ for match in re.finditer(regex.strict_username, content): - username = match.group().strip().split('@')[1:] + username = match.group().strip().split("@")[1:] if len(username) == 1: # this looks like a local user (@user), fill in the domain username.append(DOMAIN) - username = '@'.join(username) + username = "@".join(username) mention_user = handle_remote_webfinger(username) if not mention_user: @@ -97,15 +100,16 @@ def find_mentions(content): def format_links(content): - ''' detect and format links ''' + """ detect and format links """ return re.sub( - r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \ - regex.domain, + r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % regex.domain, r'\g<1>\g<3>', - content) + content, + ) + def to_markdown(content): - ''' catch links and convert to markdown ''' + """ catch links and convert to markdown """ content = markdown(content) content = format_links(content) # sanitize resulting html diff --git a/bookwyrm/views/tag.py b/bookwyrm/views/tag.py index e106e8dce..a6bdf05a2 100644 --- a/bookwyrm/views/tag.py +++ b/bookwyrm/views/tag.py @@ -1,4 +1,4 @@ -''' tagging views''' +""" tagging views""" from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse @@ -12,34 +12,35 @@ from .helpers import is_api_request # pylint: disable= no-self-use class Tag(View): - ''' tag page ''' + """ tag page """ + def get(self, request, tag_id): - ''' see books related to a tag ''' + """ see books related to a tag """ tag_obj = get_object_or_404(models.Tag, identifier=tag_id) if is_api_request(request): - return ActivitypubResponse( - tag_obj.to_activity(**request.GET)) + return ActivitypubResponse(tag_obj.to_activity(**request.GET)) books = models.Edition.objects.filter( usertag__tag__identifier=tag_id ).distinct() data = { - 'books': books, - 'tag': tag_obj, + "books": books, + "tag": tag_obj, } - return TemplateResponse(request, 'tag.html', data) + return TemplateResponse(request, "tag.html", data) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AddTag(View): - ''' add a tag to a book ''' + """ add a tag to a book """ + def post(self, request): - ''' tag a book ''' + """ tag a book """ # I'm not using a form here because sometimes "name" is sent as a hidden # field which doesn't validate - name = request.POST.get('name') - book_id = request.POST.get('book') + name = request.POST.get("name") + book_id = request.POST.get("book") book = get_object_or_404(models.Edition, id=book_id) tag_obj, _ = models.Tag.objects.get_or_create( name=name, @@ -50,21 +51,23 @@ class AddTag(View): tag=tag_obj, ) - return redirect('/book/%s' % book_id) + return redirect("/book/%s" % book_id) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveTag(View): - ''' remove a user's tag from a book ''' + """ remove a user's tag from a book """ + def post(self, request): - ''' untag a book ''' - name = request.POST.get('name') + """ untag a book """ + name = request.POST.get("name") tag_obj = get_object_or_404(models.Tag, name=name) - book_id = request.POST.get('book') + book_id = request.POST.get("book") book = get_object_or_404(models.Edition, id=book_id) user_tag = get_object_or_404( - models.UserTag, tag=tag_obj, book=book, user=request.user) + models.UserTag, tag=tag_obj, book=book, user=request.user + ) user_tag.delete() - return redirect('/book/%s' % book_id) + return redirect("/book/%s" % book_id) diff --git a/bookwyrm/views/updates.py b/bookwyrm/views/updates.py index 233e51917..83b680c0b 100644 --- a/bookwyrm/views/updates.py +++ b/bookwyrm/views/updates.py @@ -1,17 +1,20 @@ -''' endpoints for getting updates about activity ''' +""" endpoints for getting updates about activity """ from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.utils.decorators import method_decorator from django.views import View # pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class Updates(View): - ''' so the app can poll ''' + """ so the app can poll """ + def get(self, request): - ''' any notifications waiting? ''' - return JsonResponse({ - 'notifications': request.user.notification_set.filter( - read=False - ).count(), - }) + """ any notifications waiting? """ + return JsonResponse( + { + "notifications": request.user.notification_set.filter( + read=False + ).count(), + } + ) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index f3a088c13..469a82d38 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -1,4 +1,4 @@ -''' non-interactive pages ''' +""" non-interactive pages """ from io import BytesIO from uuid import uuid4 from PIL import Image @@ -22,9 +22,10 @@ from .helpers import is_blocked, object_visible_to_user # pylint: disable= no-self-use class User(View): - ''' user profile page ''' + """ user profile page """ + def get(self, request, username): - ''' profile page for a user ''' + """ profile page for a user """ try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -40,7 +41,7 @@ class User(View): # otherwise we're at a UI view try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 @@ -52,19 +53,21 @@ class User(View): if not is_self: follower = user.followers.filter(id=request.user.id).exists() if follower: - shelves = shelves.filter(privacy__in=['public', 'followers']) + shelves = shelves.filter(privacy__in=["public", "followers"]) else: - shelves = shelves.filter(privacy='public') + shelves = shelves.filter(privacy="public") for user_shelf in shelves.all(): if not user_shelf.books.count(): continue - shelf_preview.append({ - 'name': user_shelf.name, - 'local_path': user_shelf.local_path, - 'books': user_shelf.books.all()[:3], - 'size': user_shelf.books.count(), - }) + shelf_preview.append( + { + "name": user_shelf.name, + "local_path": user_shelf.local_path, + "books": user_shelf.books.all()[:3], + "size": user_shelf.books.count(), + } + ) if len(shelf_preview) > 2: break @@ -75,24 +78,27 @@ class User(View): ) paginated = Paginator(activities, PAGE_LENGTH) goal = models.AnnualGoal.objects.filter( - user=user, year=timezone.now().year).first() + user=user, year=timezone.now().year + ).first() if not object_visible_to_user(request.user, goal): goal = None data = { - 'user': user, - 'is_self': is_self, - 'shelves': shelf_preview, - 'shelf_count': shelves.count(), - 'activities': paginated.page(page), - 'goal': goal, + "user": user, + "is_self": is_self, + "shelves": shelf_preview, + "shelf_count": shelves.count(), + "activities": paginated.page(page), + "goal": goal, } - return TemplateResponse(request, 'user/user.html', data) + return TemplateResponse(request, "user/user.html", data) + class Followers(View): - ''' list of followers view ''' + """ list of followers view """ + def get(self, request, username): - ''' list of followers ''' + """ list of followers """ try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -103,20 +109,21 @@ class Followers(View): return HttpResponseNotFound() if is_api_request(request): - return ActivitypubResponse( - user.to_followers_activity(**request.GET)) + return ActivitypubResponse(user.to_followers_activity(**request.GET)) data = { - 'user': user, - 'is_self': request.user.id == user.id, - 'followers': user.followers.all(), + "user": user, + "is_self": request.user.id == user.id, + "followers": user.followers.all(), } - return TemplateResponse(request, 'user/followers.html', data) + return TemplateResponse(request, "user/followers.html", data) + class Following(View): - ''' list of following view ''' + """ list of following view """ + def get(self, request, username): - ''' list of followers ''' + """ list of followers """ try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -127,46 +134,45 @@ class Following(View): return HttpResponseNotFound() if is_api_request(request): - return ActivitypubResponse( - user.to_following_activity(**request.GET)) + return ActivitypubResponse(user.to_following_activity(**request.GET)) data = { - 'user': user, - 'is_self': request.user.id == user.id, - 'following': user.following.all(), + "user": user, + "is_self": request.user.id == user.id, + "following": user.following.all(), } - return TemplateResponse(request, 'user/following.html', data) + return TemplateResponse(request, "user/following.html", data) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class EditUser(View): - ''' edit user view ''' + """ edit user view """ + def get(self, request): - ''' edit profile page for a user ''' + """ edit profile page for a user """ data = { - 'form': forms.EditUserForm(instance=request.user), - 'user': request.user, + "form": forms.EditUserForm(instance=request.user), + "user": request.user, } - return TemplateResponse(request, 'preferences/edit_user.html', data) + return TemplateResponse(request, "preferences/edit_user.html", data) def post(self, request): - ''' les get fancy with images ''' - form = forms.EditUserForm( - request.POST, request.FILES, instance=request.user) + """ les get fancy with images """ + form = forms.EditUserForm(request.POST, request.FILES, instance=request.user) if not form.is_valid(): - data = {'form': form, 'user': request.user} - return TemplateResponse(request, 'preferences/edit_user.html', data) + data = {"form": form, "user": request.user} + return TemplateResponse(request, "preferences/edit_user.html", data) user = form.save(commit=False) - if 'avatar' in form.files: + if "avatar" in form.files: # crop and resize avatar upload - image = Image.open(form.files['avatar']) + image = Image.open(form.files["avatar"]) image = crop_avatar(image) # set the name to a hash - extension = form.files['avatar'].name.split('.')[-1] - filename = '%s.%s' % (uuid4(), extension) + extension = form.files["avatar"].name.split(".")[-1] + filename = "%s.%s" % (uuid4(), extension) user.avatar.save(filename, image) user.save() @@ -174,22 +180,27 @@ class EditUser(View): def crop_avatar(image): - ''' reduce the size and make an avatar square ''' + """ reduce the size and make an avatar square """ target_size = 120 width, height = image.size - thumbnail_scale = height / (width / target_size) if height > width \ + thumbnail_scale = ( + height / (width / target_size) + if height > width else width / (height / target_size) + ) image.thumbnail([thumbnail_scale, thumbnail_scale]) width, height = image.size width_diff = width - target_size height_diff = height - target_size - cropped = image.crop(( - int(width_diff / 2), - int(height_diff / 2), - int(width - (width_diff / 2)), - int(height - (height_diff / 2)) - )) + cropped = image.crop( + ( + int(width_diff / 2), + int(height_diff / 2), + int(width - (width_diff / 2)), + int(height - (height_diff / 2)), + ) + ) output = BytesIO() cropped.save(output, format=image.format) return ContentFile(output.getvalue()) diff --git a/bookwyrm/wellknown.py b/bookwyrm/wellknown.py index 1f6d4ccfb..eb0148082 100644 --- a/bookwyrm/wellknown.py +++ b/bookwyrm/wellknown.py @@ -1,4 +1,4 @@ -''' responds to various requests to /.well-know ''' +""" responds to various requests to /.well-know """ from dateutil.relativedelta import relativedelta from django.http import HttpResponseNotFound @@ -10,50 +10,54 @@ from bookwyrm.settings import DOMAIN, VERSION def webfinger(request): - ''' allow other servers to ask about a user ''' - if request.method != 'GET': + """ allow other servers to ask about a user """ + if request.method != "GET": return HttpResponseNotFound() - resource = request.GET.get('resource') - if not resource and not resource.startswith('acct:'): + resource = request.GET.get("resource") + if not resource and not resource.startswith("acct:"): return HttpResponseNotFound() - username = resource.replace('acct:', '') + username = resource.replace("acct:", "") try: user = models.User.objects.get(username=username) except models.User.DoesNotExist: - return HttpResponseNotFound('No account found') + return HttpResponseNotFound("No account found") - return JsonResponse({ - 'subject': 'acct:%s' % (user.username), - 'links': [ - { - 'rel': 'self', - 'type': 'application/activity+json', - 'href': user.remote_id - } - ] - }) + return JsonResponse( + { + "subject": "acct:%s" % (user.username), + "links": [ + { + "rel": "self", + "type": "application/activity+json", + "href": user.remote_id, + } + ], + } + ) def nodeinfo_pointer(request): - ''' direct servers to nodeinfo ''' - if request.method != 'GET': + """ direct servers to nodeinfo """ + if request.method != "GET": return HttpResponseNotFound() - return JsonResponse({ - 'links': [ - { - 'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href': 'https://%s/nodeinfo/2.0' % DOMAIN - } - ] - }) + return JsonResponse( + { + "links": [ + { + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", + "href": "https://%s/nodeinfo/2.0" % DOMAIN, + } + ] + } + ) def nodeinfo(request): - ''' basic info about the server ''' - if request.method != 'GET': + """ basic info about the server """ + if request.method != "GET": return HttpResponseNotFound() status_count = models.Status.objects.filter(user__local=True).count() @@ -61,70 +65,65 @@ def nodeinfo(request): month_ago = timezone.now() - relativedelta(months=1) last_month_count = models.User.objects.filter( - local=True, - last_active_date__gt=month_ago + local=True, last_active_date__gt=month_ago ).count() six_months_ago = timezone.now() - relativedelta(months=6) six_month_count = models.User.objects.filter( - local=True, - last_active_date__gt=six_months_ago + local=True, last_active_date__gt=six_months_ago ).count() site = models.SiteSettings.get() - return JsonResponse({ - 'version': '2.0', - 'software': { - 'name': 'bookwyrm', - 'version': VERSION - }, - 'protocols': [ - 'activitypub' - ], - 'usage': { - 'users': { - 'total': user_count, - 'activeMonth': last_month_count, - 'activeHalfyear': six_month_count, + return JsonResponse( + { + "version": "2.0", + "software": {"name": "bookwyrm", "version": VERSION}, + "protocols": ["activitypub"], + "usage": { + "users": { + "total": user_count, + "activeMonth": last_month_count, + "activeHalfyear": six_month_count, + }, + "localPosts": status_count, }, - 'localPosts': status_count, - }, - 'openRegistrations': site.allow_registration, - }) + "openRegistrations": site.allow_registration, + } + ) def instance_info(request): - ''' let's talk about your cool unique instance ''' - if request.method != 'GET': + """ let's talk about your cool unique instance """ + if request.method != "GET": return HttpResponseNotFound() user_count = models.User.objects.filter(local=True).count() status_count = models.Status.objects.filter(user__local=True).count() site = models.SiteSettings.get() - return JsonResponse({ - 'uri': DOMAIN, - 'title': site.name, - 'short_description': '', - 'description': site.instance_description, - 'version': '0.0.1', - 'stats': { - 'user_count': user_count, - 'status_count': status_count, - }, - 'thumbnail': 'https://%s/static/images/logo.png' % DOMAIN, - 'languages': [ - 'en' - ], - 'registrations': site.allow_registration, - 'approval_required': False, - }) + return JsonResponse( + { + "uri": DOMAIN, + "title": site.name, + "short_description": "", + "description": site.instance_description, + "version": "0.0.1", + "stats": { + "user_count": user_count, + "status_count": status_count, + }, + "thumbnail": "https://%s/static/images/logo.png" % DOMAIN, + "languages": ["en"], + "registrations": site.allow_registration, + "approval_required": False, + } + ) def peers(request): - ''' list of federated servers this instance connects with ''' - if request.method != 'GET': + """ list of federated servers this instance connects with """ + if request.method != "GET": return HttpResponseNotFound() - names = models.FederatedServer.objects.values_list('server_name', flat=True) + names = models.FederatedServer.objects.values_list("server_name", flat=True) return JsonResponse(list(names), safe=False) diff --git a/celerywyrm/__init__.py b/celerywyrm/__init__.py index 1c8fa15d1..fe0c87ff1 100644 --- a/celerywyrm/__init__.py +++ b/celerywyrm/__init__.py @@ -1,8 +1,8 @@ -''' we need this file to initialize celery ''' +""" we need this file to initialize celery """ from __future__ import absolute_import, unicode_literals # This will make sure the app is always imported when # Django starts so that shared_task will use this app. from .celery import app as celery_app -__all__ = ('celery_app',) +__all__ = ("celery_app",) diff --git a/celerywyrm/asgi.py b/celerywyrm/asgi.py index c03a6ec6b..0f0b00219 100644 --- a/celerywyrm/asgi.py +++ b/celerywyrm/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'celerywyrm.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings") application = get_asgi_application() diff --git a/celerywyrm/celery.py b/celerywyrm/celery.py index 2937ef0fc..4af8e281d 100644 --- a/celerywyrm/celery.py +++ b/celerywyrm/celery.py @@ -1,4 +1,4 @@ -''' configures celery for task management ''' +""" configures celery for task management """ from __future__ import absolute_import, unicode_literals import os @@ -7,23 +7,22 @@ from . import settings # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'celerywyrm.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings") -app = Celery('celerywyrm') +app = Celery("celerywyrm") # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object("django.conf:settings", namespace="CELERY") # Load task modules from all registered Django app configs. app.autodiscover_tasks() -app.autodiscover_tasks(['bookwyrm'], related_name='activitypub.base_activity') -app.autodiscover_tasks(['bookwyrm'], related_name='broadcast') -app.autodiscover_tasks( - ['bookwyrm'], related_name='connectors.abstract_connector') -app.autodiscover_tasks(['bookwyrm'], related_name='emailing') -app.autodiscover_tasks(['bookwyrm'], related_name='goodreads_import') -app.autodiscover_tasks(['bookwyrm'], related_name='models.user') -app.autodiscover_tasks(['bookwyrm'], related_name='views.inbox') +app.autodiscover_tasks(["bookwyrm"], related_name="activitypub.base_activity") +app.autodiscover_tasks(["bookwyrm"], related_name="broadcast") +app.autodiscover_tasks(["bookwyrm"], related_name="connectors.abstract_connector") +app.autodiscover_tasks(["bookwyrm"], related_name="emailing") +app.autodiscover_tasks(["bookwyrm"], related_name="goodreads_import") +app.autodiscover_tasks(["bookwyrm"], related_name="models.user") +app.autodiscover_tasks(["bookwyrm"], related_name="views.inbox") diff --git a/celerywyrm/settings.py b/celerywyrm/settings.py index 92986d8e4..7591163b1 100644 --- a/celerywyrm/settings.py +++ b/celerywyrm/settings.py @@ -16,31 +16,31 @@ from environs import Env env = Env() # emailing -EMAIL_HOST = env('EMAIL_HOST') -EMAIL_PORT = env('EMAIL_PORT') -EMAIL_HOST_USER = env('EMAIL_HOST_USER') -EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') -EMAIL_USE_TLS = env('EMAIL_USE_TLS') +EMAIL_HOST = env("EMAIL_HOST") +EMAIL_PORT = env("EMAIL_PORT") +EMAIL_HOST_USER = env("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") +EMAIL_USE_TLS = env("EMAIL_USE_TLS") # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # celery/rebbitmq -CELERY_BROKER_URL = env('CELERY_BROKER') -CELERY_ACCEPT_CONTENT = ['json'] -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_BACKEND = 'redis' +CELERY_BROKER_URL = env("CELERY_BROKER") +CELERY_ACCEPT_CONTENT = ["json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_BACKEND = "redis" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '0a^0gpwjc1ap+lb$dinin=efc@e&_0%102$o3(>9e7lndiaw' +SECRET_KEY = "0a^0gpwjc1ap+lb$dinin=efc@e&_0%102$o3(>9e7lndiaw" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool('DEBUG', True) +DEBUG = env.bool("DEBUG", True) ALLOWED_HOSTS = [] @@ -49,71 +49,69 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'celerywyrm', - 'bookwyrm', - 'celery', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "celerywyrm", + "bookwyrm", + "celery", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'celerywyrm.urls' +ROOT_URLCONF = "celerywyrm.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'celerywyrm.wsgi.application' +WSGI_APPLICATION = "celerywyrm.wsgi.application" # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases -BOOKWYRM_DATABASE_BACKEND = env('BOOKWYRM_DATABASE_BACKEND', 'postgres') +BOOKWYRM_DATABASE_BACKEND = env("BOOKWYRM_DATABASE_BACKEND", "postgres") BOOKWYRM_DBS = { - 'postgres': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': env('POSTGRES_DB', 'fedireads'), - 'USER': env('POSTGRES_USER', 'fedireads'), - 'PASSWORD': env('POSTGRES_PASSWORD', 'fedireads'), - 'HOST': env('POSTGRES_HOST', ''), - 'PORT': 5432 + "postgres": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": env("POSTGRES_DB", "fedireads"), + "USER": env("POSTGRES_USER", "fedireads"), + "PASSWORD": env("POSTGRES_PASSWORD", "fedireads"), + "HOST": env("POSTGRES_HOST", ""), + "PORT": 5432, + }, + "sqlite": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "fedireads.db"), }, - 'sqlite': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'fedireads.db') - } } -DATABASES = { - 'default': BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND] -} +DATABASES = {"default": BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND]} # Password validation @@ -121,16 +119,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -138,9 +136,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = False @@ -152,7 +150,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, env('STATIC_ROOT', 'static')) -MEDIA_URL = '/images/' -MEDIA_ROOT = os.path.join(BASE_DIR, env('MEDIA_ROOT', 'images')) +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) +MEDIA_URL = "/images/" +MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) diff --git a/celerywyrm/urls.py b/celerywyrm/urls.py index 21c8dee86..394c0ef0d 100644 --- a/celerywyrm/urls.py +++ b/celerywyrm/urls.py @@ -20,5 +20,5 @@ from django.urls import path from celerywyrm import settings urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/celerywyrm/wsgi.py b/celerywyrm/wsgi.py index 6de8d633f..7ccf36a9f 100644 --- a/celerywyrm/wsgi.py +++ b/celerywyrm/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'celerywyrm.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings") application = get_wsgi_application() From d3162e12db9d4f9ae975253f5fe10e21747f6c0a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 08:51:54 -0800 Subject: [PATCH 026/145] 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 6a28ba9d6..484c695b0 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 027/145] 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 b920fc9c0..1ab6e0eef 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 a4d62efda..1ebcc6b9a 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 4e8481f09..13497df83 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 25b44fb4a..0000015e5 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 5f03e1a91..a115ac493 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 3bdfc341e410560fc6b2b68504a65f12ce851403 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 09:54:02 -0800 Subject: [PATCH 028/145] Runs black --- bookwyrm/activitypub/note.py | 6 +-- bookwyrm/migrations/0046_reviewrating.py | 43 +++++++++++++------ .../migrations/0047_merge_20210228_1839.py | 7 ++- bookwyrm/models/status.py | 8 ++-- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index bea275d16..a739eafa1 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -13,9 +13,9 @@ class Tombstone(ActivityObject): type: str = "Tombstone" - def to_model(self, *args, **kwargs):# pylint: disable=unused-argument - ''' this should never really get serialized, just searched for ''' - model = apps.get_model('bookwyrm.Status') + def to_model(self, *args, **kwargs): # pylint: disable=unused-argument + """ this should never really get serialized, just searched for """ + model = apps.get_model("bookwyrm.Status") return model.find_existing_by_remote_id(self.id) diff --git a/bookwyrm/migrations/0046_reviewrating.py b/bookwyrm/migrations/0046_reviewrating.py index e410ba91f..8d1490042 100644 --- a/bookwyrm/migrations/0046_reviewrating.py +++ b/bookwyrm/migrations/0046_reviewrating.py @@ -6,44 +6,61 @@ from django.db.models import Q import django.db.models.deletion from psycopg2.extras import execute_values + def convert_review_rating(app_registry, schema_editor): - ''' take rating type Reviews and convert them to ReviewRatings ''' + """ take rating type Reviews and convert them to ReviewRatings """ db_alias = schema_editor.connection.alias - reviews = app_registry.get_model( - 'bookwyrm', 'Review' - ).objects.using(db_alias).filter( - Q(content__isnull=True) | Q(content='') + reviews = ( + app_registry.get_model("bookwyrm", "Review") + .objects.using(db_alias) + .filter(Q(content__isnull=True) | Q(content="")) ) with connection.cursor() as cursor: values = [(r.id,) for r in reviews] - execute_values(cursor, ''' + execute_values( + cursor, + """ INSERT INTO bookwyrm_reviewrating(review_ptr_id) -VALUES %s''', values) +VALUES %s""", + values, + ) + def unconvert_review_rating(app_registry, schema_editor): - ''' undo the conversion from ratings back to reviews''' + """ undo the conversion from ratings back to reviews""" # All we need to do to revert this is drop the table, which Django will do # on its own, as long as we have a valid reverse function. So, this is a # no-op function so Django will do its thing + class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0045_auto_20210210_2114'), + ("bookwyrm", "0045_auto_20210210_2114"), ] operations = [ migrations.CreateModel( - name='ReviewRating', + name="ReviewRating", fields=[ - ('review_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Review')), + ( + "review_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Review", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.review',), + bases=("bookwyrm.review",), ), migrations.RunPython(convert_review_rating, unconvert_review_rating), ] diff --git a/bookwyrm/migrations/0047_merge_20210228_1839.py b/bookwyrm/migrations/0047_merge_20210228_1839.py index 7e410761a..4be39e56f 100644 --- a/bookwyrm/migrations/0047_merge_20210228_1839.py +++ b/bookwyrm/migrations/0047_merge_20210228_1839.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0046_reviewrating'), - ('bookwyrm', '0046_sitesettings_privacy_policy'), + ("bookwyrm", "0046_reviewrating"), + ("bookwyrm", "0046_sitesettings_privacy_policy"), ] - operations = [ - ] + operations = [] diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 9378772c6..80f2b593a 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -295,11 +295,11 @@ class Review(Status): class ReviewRating(Review): - ''' a subtype of review that only contains a rating ''' + """ a subtype of review that only contains a rating """ + def save(self, *args, **kwargs): if not self.rating: - raise ValueError( - 'ReviewRating object must include a numerical rating') + raise ValueError("ReviewRating object must include a numerical rating") return super().save(*args, **kwargs) @property @@ -307,7 +307,7 @@ class ReviewRating(Review): return 'Rated "{}": {:d} stars'.format(self.book.title, self.rating) activity_serializer = activitypub.Rating - pure_type = 'Note' + pure_type = "Note" class Boost(ActivityMixin, Status): From 84534a299164f3acc2c819100e77484c833d535c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 09:54:53 -0800 Subject: [PATCH 029/145] Adds merge migration --- bookwyrm/migrations/0048_merge_20210308_1754.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 bookwyrm/migrations/0048_merge_20210308_1754.py diff --git a/bookwyrm/migrations/0048_merge_20210308_1754.py b/bookwyrm/migrations/0048_merge_20210308_1754.py new file mode 100644 index 000000000..6c4488a4f --- /dev/null +++ b/bookwyrm/migrations/0048_merge_20210308_1754.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.7 on 2021-03-08 17:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0047_connector_isbn_search_url'), + ('bookwyrm', '0047_merge_20210228_1839'), + ] + + operations = [ + ] From 5fe989e20f39f9edd4a3b3f7dee7de810c6d6d96 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 09:58:07 -0800 Subject: [PATCH 030/145] Formats migrationwq --- bookwyrm/migrations/0048_merge_20210308_1754.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bookwyrm/migrations/0048_merge_20210308_1754.py b/bookwyrm/migrations/0048_merge_20210308_1754.py index 6c4488a4f..47fa9e771 100644 --- a/bookwyrm/migrations/0048_merge_20210308_1754.py +++ b/bookwyrm/migrations/0048_merge_20210308_1754.py @@ -6,9 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0047_connector_isbn_search_url'), - ('bookwyrm', '0047_merge_20210228_1839'), + ("bookwyrm", "0047_connector_isbn_search_url"), + ("bookwyrm", "0047_merge_20210228_1839"), ] - operations = [ - ] + operations = [] From acbebbe94768bdefe763424ff157a6f3a13059bd Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 10:10:30 -0800 Subject: [PATCH 031/145] 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 09e13ae50..99c45ed22 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 feadf8622..024937691 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 246ac8af3..ed752e75a 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 7c3ec3487..55cc75b43 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 74c42fbbc..712b80287 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 031235009f0f10ca055704c14ffbe33a03b05799 Mon Sep 17 00:00:00 2001 From: erion Date: Mon, 8 Mar 2021 19:20:55 +0100 Subject: [PATCH 032/145] Fix albel for the OCLC number field. --- 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 4d2159497..7374e6a00 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -104,7 +104,7 @@ {% for error in form.openlibrary_key.errors %}

{{ error | escape }}

{% endfor %} -

{{ form.oclc_number }}

+

{{ form.oclc_number }}

{% for error in form.oclc_number.errors %}

{{ error | escape }}

{% endfor %} From 58b48faff870723a8aba0bf745756c82c266c6ed Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 8 Mar 2021 10:48:45 -0800 Subject: [PATCH 033/145] 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 024937691..90b7359d4 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 55cc75b43..ecba43766 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 034/145] 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 90b7359d4..bba83714e 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 035/145] 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 bba83714e..1549bdc64 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 ecba43766..ae60c677d 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 036/145] 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 1ebcc6b9a..700dc5704 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 037/145] 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 901a12ff5..fc2ebdb74 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 16741436c..312d502a7 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 000000000..67bffe275 --- /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 000000000..efca7244b --- /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 babd82961..0f099e177 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 b3f667823..c9fdac0a4 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 844f89937..c7ec7f4b7 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 48da8ec1a..36a64ddfe 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 000000000..a90fffaf4 --- /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 038/145] 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 000000000..3b8dec260 --- /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 039/145] 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 1384056f2..4b5b7fef2 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 000000000..deb8ba6f6 --- /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 67ee16d3d..326a673e1 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 ce8043102..8f7d903e4 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 000000000..afbc356b5 --- /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 3b8dec260..d35633cbf 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 a90fffaf4..2e374d5ea 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 040/145] 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 380e701fa..654130cf9 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 deb8ba6f6..494f5bc8c 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 afbc356b5..e1e8c2a41 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 000000000..1f55906bd --- /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 67bffe275..b72dd958d 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 000000000..5fbaee99b --- /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 0f099e177..f703ba6e1 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 c9fdac0a4..585417c71 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 d35633cbf..70414dcb6 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 c7ec7f4b7..42b9803d3 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 36a64ddfe..b433dca24 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 2e374d5ea..9eaf9bdc9 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 041/145] 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 312d502a7..a34fe6389 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 b72dd958d..63eca0fca 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 efca7244b..59fbaf568 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 59f1d567eb05f6159d0474be3d814b0cf68e9cb6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 9 Mar 2021 10:35:35 -0800 Subject: [PATCH 042/145] Updates README on main for production changes --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 41bd70653..98ce9c766 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,6 @@ Instructions for running BookWyrm in production: - Comment out the `command: certonly...` line in `docker-compose.yml` - Run docker-compose in the background with: `docker-compose up -d` - Initialize the database with: `./bw-dev initdb` - - Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U ` and saves the backup to a safe location Congrats! You did it, go to your domain and enjoy the fruits of your labors. @@ -205,3 +204,16 @@ There are three concepts in the book data model: Whenever a user interacts with a book, they are interacting with a specific edition. Every work has a default edition, but the user can select other editions. Reviews aggregated for all editions of a work when you view an edition's page. +### Backups + +Bookwyrm's db service dumps a backup copy of its database to its `/backups` directory daily at midnight UTC. +Backups are named `backup__%Y-%m-%d.sql`. + +The db service has an optional script for periodically pruning the backups directory so that all recent daily backups are kept, but for older backups, only weekly or monthly backups are kept. +To enable this script: +- Uncomment the final line in `postgres-docker/cronfile` +- rebuild your instance `docker-compose up --build` + +You can copy backups from the backups volume to your host machine with `docker cp`: +- Run `docker-compose ps` to confirm the db service's full name (it's probably `bookwyrm_db_1`. +- Run `docker cp :/backups From 999bff4bbadb25f0e1a3015a4ece75bcc0559baa Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 9 Mar 2021 12:35:07 -0800 Subject: [PATCH 043/145] 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 1f55906bd..74c3641bb 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 63eca0fca..25cb8a2fb 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 59fbaf568..329901eab 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 9eaf9bdc9..eda565329 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 044/145] 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 494f5bc8c..ae9d77a89 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 e1e8c2a41..3a8fdd13a 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 045/145] 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 74c3641bb..ca68d51d8 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 329901eab..ebf29a7a7 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 5fbaee99b..9d32d5fb9 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 eda565329..875470811 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 046/145] 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 700dc5704..e42f77f2d 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 ae60c677d..9048f43dd 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 047/145] 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 e42f77f2d..107e75c56 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 9048f43dd..ff0d67647 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 048/145] 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 ff0d67647..1cb21f5e7 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 049/145] 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 3a8fdd13a..8893f42fc 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 ca68d51d8..ce0a0b3a5 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 000000000..286131301 --- /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 25cb8a2fb..9acc4f771 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 80ee22508..3f0300bd8 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 9d32d5fb9..2fa0a3f30 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 8d6c21ed9..f3732d1a3 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 70414dcb6..1a5bfd158 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 42b9803d3..551be1e30 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 b433dca24..63ab98e54 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 875470811..59b9b9e40 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 053/145] 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 59b9b9e40..ad22da2ba 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 054/145] 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 ce0a0b3a5..32fdf6d74 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 3c7100558..6f0ca2e6e 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 551be1e30..0ad464f41 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 055/145] 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 32fdf6d74..b2e61cc44 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 3888be27c..c35010cf6 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 1a5bfd158..724e7b5a6 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 0ad464f41..26ce67a3c 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 63ab98e54..606624b84 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 ad22da2ba..2ba0d27ac 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 056/145] 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 b2e61cc44..ae014d68c 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 c35010cf6..3a5ebcf95 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 f3732d1a3..a7e8e8843 100644 --- a/bookwyrm/templates/snippets/status/status_body.html +++ b/bookwyrm/templates/snippets/status/status_body.html @@ -19,8 +19,15 @@ {% block card-footer %}