From 68813f9453af409b9f9470f7cfe2efbd1c3a0175 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Oct 2020 14:17:04 -0700 Subject: [PATCH 001/416] Nginx and certbot config for prod deploy --- nginx/Dockerfile | 4 -- nginx/{nginx.conf => default.conf} | 0 nginx/prod-default.conf | 44 ++++++++++++++++++ prod-docker-compose.yml | 74 ++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 4 deletions(-) delete mode 100644 nginx/Dockerfile rename nginx/{nginx.conf => default.conf} (100%) create mode 100644 nginx/prod-default.conf create mode 100644 prod-docker-compose.yml diff --git a/nginx/Dockerfile b/nginx/Dockerfile deleted file mode 100644 index 66074cf6..00000000 --- a/nginx/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM nginx:1.17.4-alpine - -RUN rm /etc/nginx/conf.d/default.conf -COPY nginx.conf /etc/nginx/conf.d diff --git a/nginx/nginx.conf b/nginx/default.conf similarity index 100% rename from nginx/nginx.conf rename to nginx/default.conf diff --git a/nginx/prod-default.conf b/nginx/prod-default.conf new file mode 100644 index 00000000..079a7aaf --- /dev/null +++ b/nginx/prod-default.conf @@ -0,0 +1,44 @@ +upstream web { + server web:8000; +} + +server { + listen [::]:80; + listen 80; + + server_name bookwyrm.social www.bookwyrm.social; + + location ~ /.well-known/acme-challenge { + allow all; + root /var/www/certbot; + } + + # redirect http to https www + return 301 https://www.bookwyrm.social$request_uri; +} + +server { + listen [::]:443 ssl http2; + listen 443 ssl http2; + + server_name bookwyrm.social; + + # SSL code + ssl_certificate /etc/nginx/ssl/live/bookwyrm.social/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/bookwyrm.social/privkey.pem; + + location / { + proxy_pass http://web; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + } + + location /images/ { + alias /app/images/; + } + + location /static/ { + alias /app/static/; + } +} diff --git a/prod-docker-compose.yml b/prod-docker-compose.yml new file mode 100644 index 00000000..5f5ac9c6 --- /dev/null +++ b/prod-docker-compose.yml @@ -0,0 +1,74 @@ +version: '3' + +services: + nginx: + image: nginx:latest + ports: + - 80:80 + - 443:443 + depends_on: + - web + networks: + - main + volumes: + - ./nginx:/etc/nginx/conf.d + - ./certbot/conf:/etc/nginx/ssl + - ./certbot/data:/var/www/certbot + - static_volume:/app/static + - media_volume:/app/images + certbot: + image: certbot/certbot:latest + command: certonly --webroot --webroot-path=/var/www/certbot --email mouse.reeve@gmail.com --agree-tos --no-eff-email -d bookwyrm.social -d www.bookwyrm.social + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/logs:/var/log/letsencrypt + - ./certbot/data:/var/www/certbot + db: + image: postgres + env_file: .env + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - main + web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/app + - static_volume:/app/static + - media_volume:/app/images + depends_on: + - db + - celery_worker + networks: + - main + ports: + - 8000:8000 + redis: + image: redis + env_file: .env + ports: + - "6379:6379" + networks: + - main + restart: on-failure + celery_worker: + env_file: .env + build: . + networks: + - main + command: celery -A celerywyrm worker -l info + volumes: + - .:/app + - static_volume:/app/static + - media_volume:/app/images + depends_on: + - db + - redis + restart: on-failure +volumes: + pgdata: + static_volume: + media_volume: +networks: + main: From e24eca7da0a8b49c22f2e650ce0cf918cf53d713 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Oct 2020 14:22:37 -0700 Subject: [PATCH 002/416] Config files for prod deployment --- docker-compose.yml | 3 ++- nginx/default.conf | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6f9dbdc2..d7c4ec3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: nginx: - build: ./nginx + image: nginx:latest ports: - 1333:80 depends_on: @@ -10,6 +10,7 @@ services: networks: - main volumes: + - ./nginx:/etc/nginx/conf.d - static_volume:/app/static - media_volume:/app/images db: diff --git a/nginx/default.conf b/nginx/default.conf index d3898287..396852e2 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -20,3 +20,46 @@ server { alias /app/static/; } } + +# PROD version +# +#server { +# listen [::]:80; +# listen 80; +# +# server_name you-domain.com www.you-domain.com; +# +# location ~ /.well-known/acme-challenge { +# allow all; +# root /var/www/certbot; +# } +# +# # redirect http to https www +# return 301 https://www.you-domain.com$request_uri; +#} +# +#server { +# listen [::]:443 ssl http2; +# listen 443 ssl http2; +# +# server_name you-domain.com; +# +# # SSL code +# ssl_certificate /etc/nginx/ssl/live/you-domain.com/fullchain.pem; +# ssl_certificate_key /etc/nginx/ssl/live/you-domain.com/privkey.pem; +# +# location / { +# proxy_pass http://web; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header Host $host; +# proxy_redirect off; +# } +# +# location /images/ { +# alias /app/images/; +# } +# +# location /static/ { +# alias /app/static/; +# } +#} From d29ed2746ab03424900e451bee7f8f5c44d752fb Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Oct 2020 14:24:14 -0700 Subject: [PATCH 003/416] Removed old prod nginx conf --- nginx/prod-default.conf | 44 ----------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 nginx/prod-default.conf diff --git a/nginx/prod-default.conf b/nginx/prod-default.conf deleted file mode 100644 index 079a7aaf..00000000 --- a/nginx/prod-default.conf +++ /dev/null @@ -1,44 +0,0 @@ -upstream web { - server web:8000; -} - -server { - listen [::]:80; - listen 80; - - server_name bookwyrm.social www.bookwyrm.social; - - location ~ /.well-known/acme-challenge { - allow all; - root /var/www/certbot; - } - - # redirect http to https www - return 301 https://www.bookwyrm.social$request_uri; -} - -server { - listen [::]:443 ssl http2; - listen 443 ssl http2; - - server_name bookwyrm.social; - - # SSL code - ssl_certificate /etc/nginx/ssl/live/bookwyrm.social/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/live/bookwyrm.social/privkey.pem; - - location / { - proxy_pass http://web; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; - proxy_redirect off; - } - - location /images/ { - alias /app/images/; - } - - location /static/ { - alias /app/static/; - } -} From ba396f19a643de368c32df50d04ef2bb7faa6c01 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Oct 2020 14:25:53 -0700 Subject: [PATCH 004/416] typos in example domain --- nginx/default.conf | 10 +++++----- prod-docker-compose.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nginx/default.conf b/nginx/default.conf index 396852e2..51165243 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -27,7 +27,7 @@ server { # listen [::]:80; # listen 80; # -# server_name you-domain.com www.you-domain.com; +# server_name your-domain.com www.your-domain.com; # # location ~ /.well-known/acme-challenge { # allow all; @@ -35,18 +35,18 @@ server { # } # # # redirect http to https www -# return 301 https://www.you-domain.com$request_uri; +# return 301 https://www.your-domain.com$request_uri; #} # #server { # listen [::]:443 ssl http2; # listen 443 ssl http2; # -# server_name you-domain.com; +# server_name your-domain.com; # # # SSL code -# ssl_certificate /etc/nginx/ssl/live/you-domain.com/fullchain.pem; -# ssl_certificate_key /etc/nginx/ssl/live/you-domain.com/privkey.pem; +# ssl_certificate /etc/nginx/ssl/live/your-domain.com/fullchain.pem; +# ssl_certificate_key /etc/nginx/ssl/live/your-domain.com/privkey.pem; # # location / { # proxy_pass http://web; diff --git a/prod-docker-compose.yml b/prod-docker-compose.yml index 5f5ac9c6..0ace0df0 100644 --- a/prod-docker-compose.yml +++ b/prod-docker-compose.yml @@ -18,7 +18,7 @@ services: - media_volume:/app/images certbot: image: certbot/certbot:latest - command: certonly --webroot --webroot-path=/var/www/certbot --email mouse.reeve@gmail.com --agree-tos --no-eff-email -d bookwyrm.social -d www.bookwyrm.social + command: certonly --webroot --webroot-path=/var/www/certbot --email your-email@domain.com --agree-tos --no-eff-email -d your-domain.com -d www.your-domain.com volumes: - ./certbot/conf:/etc/letsencrypt - ./certbot/logs:/var/log/letsencrypt From d8800b09c4b7e202c8c638591811dc75a8a2176e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Oct 2020 14:58:57 -0700 Subject: [PATCH 005/416] use remote id for followers links this should be stored in the db --- bookwyrm/templates/user_header.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/user_header.html b/bookwyrm/templates/user_header.html index 6f499e39..6c501c6a 100644 --- a/bookwyrm/templates/user_header.html +++ b/bookwyrm/templates/user_header.html @@ -25,8 +25,8 @@

{{ user.username }}

Joined {{ user.created_date | naturaltime }}

- {{ user.followers.count }} follower{{ user.followers.count | pluralize }}, - {{ user.following.count }} following

+ {{ user.followers.count }} follower{{ user.followers.count | pluralize }}, + {{ user.following.count }} following

From 51e9977d558d218431c1af121e974425ed5a1aba Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 5 Oct 2020 15:23:39 -0700 Subject: [PATCH 006/416] Received bytes, expecting a string This doesn't seem like a *good* solution, but I'm not sure why sometimes this receives strings and sometimes bytes (maybe it's based on how the data is served). --- bookwyrm/incoming.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 144400f9..54e2fb24 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -37,7 +37,10 @@ def shared_inbox(request): return HttpResponseNotFound() try: - activity = json.loads(request.body) + resp = request.body + if isinstance(resp, bytes): + resp = json.loads(resp) + activity = json.loads(resp) activity_object = activity['object'] except (json.decoder.JSONDecodeError, KeyError): return HttpResponseBadRequest() From 704e1092c42b5905e6cb8631274ed20a5d58b538 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Oct 2020 12:32:45 -0700 Subject: [PATCH 007/416] Delete statuses --- bookwyrm/activitypub/__init__.py | 1 + bookwyrm/activitypub/note.py | 8 ++++++++ bookwyrm/models/status.py | 16 +++++++++++++++- bookwyrm/outgoing.py | 7 +++++++ bookwyrm/status.py | 5 +++++ bookwyrm/templates/snippets/status.html | 11 +++++++++++ bookwyrm/urls.py | 4 +++- bookwyrm/view_actions.py | 21 +++++++++++++++++++++ 8 files changed, 71 insertions(+), 2 deletions(-) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 03c714a6..45cd42a5 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -4,6 +4,7 @@ import sys from .base_activity import ActivityEncoder, Image, PublicKey, Signature from .note import Note, GeneratedNote, Article, Comment, Review, Quotation +from .note import Tombstone from .interaction import Boost, Like from .ordered_collection import OrderedCollection, OrderedCollectionPage from .person import Person diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 63ac8a6e..54730fb6 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -4,6 +4,14 @@ from typing import Dict, List from .base_activity import ActivityObject, Image +@dataclass(init=False) +class Tombstone(ActivityObject): + url: str + published: str + deleted: str + type: str = 'Tombstone' + + @dataclass(init=False) class Note(ActivityObject): ''' Note activity ''' diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 6c3369f2..f9f90467 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -22,6 +22,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): sensitive = models.BooleanField(default=False) # the created date can't be this, because of receiving federated posts published_date = models.DateTimeField(default=timezone.now) + deleted = models.BooleanField(default=False) + deleted_date = models.DateTimeField(default=timezone.now) favorites = models.ManyToManyField( 'User', symmetrical=False, @@ -104,6 +106,18 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): **kwargs ) + def to_activity(self, **kwargs): + ''' return tombstone if the status is deleted ''' + if self.deleted: + return activitypub.Tombstone( + id=self.remote_id, + url=self.remote_id, + deleted=http_date(self.deleted_date.timestamp()), + published=http_date(self.deleted_date.timestamp()), + ).serialize() + return ActivitypubMixin.to_activity(self, **kwargs) + + class GeneratedStatus(Status): ''' these are app-generated messages about user activity ''' @property @@ -112,7 +126,7 @@ class GeneratedStatus(Status): message = self.content books = ', '.join( '"%s"' % (self.book.local_id, self.book.title) \ - for book in self.mention_books + for book in self.mention_books.all() ) return '%s %s' % (message, books) diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 25a61c46..92187ffa 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -13,6 +13,7 @@ from bookwyrm.status import create_review, create_status from bookwyrm.status import create_quotation, create_comment from bookwyrm.status import create_tag, create_notification, create_rating from bookwyrm.status import create_generated_note +from bookwyrm.status import delete_status from bookwyrm.remote_user import get_or_create_remote_user @@ -197,6 +198,12 @@ def handle_import_books(user, items): return None +def handle_delete_status(user, status): + ''' delete a status and broadcast deletion to other servers ''' + delete_status(status) + broadcast(user, status.to_activity()) + + def handle_rate(user, book, rating): ''' a review that's just a rating ''' builder = create_rating diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 0c13638e..190f5dd7 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -6,6 +6,11 @@ from bookwyrm.books_manager import get_or_create_book from bookwyrm.sanitize_html import InputHtmlParser +def delete_status(status): + ''' replace the status with a tombstone ''' + status.deleted = True + status.save() + def create_rating(user, book, rating): ''' a review that's just a rating ''' if not rating or rating < 1 or rating > 5: diff --git a/bookwyrm/templates/snippets/status.html b/bookwyrm/templates/snippets/status.html index f1fab742..809cbe9e 100644 --- a/bookwyrm/templates/snippets/status.html +++ b/bookwyrm/templates/snippets/status.html @@ -25,6 +25,17 @@ Public post + {% if status.user == request.user %} +
+ {% csrf_token %} + + +
+ {% endif %} {{ status.published_date | naturaltime }} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index f1b33877..331efee5 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -11,7 +11,7 @@ localname_regex = r'(?P[\w\-_]+)' user_path = r'^user/%s' % username_regex local_user_path = r'^user/%s' % localname_regex -status_types = ['status', 'review', 'comment', 'quotation', 'boost'] +status_types = ['status', 'review', 'comment', 'quotation', 'boost', 'generatedstatus'] status_path = r'%s/(%s)/(?P\d+)' % \ (local_user_path, '|'.join(status_types)) @@ -107,6 +107,8 @@ urlpatterns = [ re_path(r'^unfavorite/(?P\d+)/?$', actions.unfavorite), re_path(r'^boost/(?P\d+)/?$', actions.boost), + re_path(r'^delete-status/?$', actions.delete_status), + re_path(r'^shelve/?$', actions.shelve), re_path(r'^follow/?$', actions.follow), diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 992a270d..e7674bb9 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -418,6 +418,27 @@ def boost(request, status_id): outgoing.handle_boost(request.user, status) return redirect(request.headers.get('Referer', '/')) + +@login_required +def delete_status(request): + ''' delete and tombstone a status ''' + status_id = request.POST.get('status') + if not status_id: + return HttpResponseBadRequest() + try: + status = models.Status.objects.get(id=status_id) + except models.Status.DoesNotExist: + return HttpResponseBadRequest() + + # don't let people delete other people's statuses + if status.user != request.user: + return HttpResponseBadRequest() + + # perform deletion + outgoing.handle_delete_status(request.user, status) + return redirect(request.headers.get('Referer', '/')) + + @login_required def follow(request): ''' follow another user, here or abroad ''' From 48df06aea79a8b2718eec9ff6cb45348510550a9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Oct 2020 12:35:27 -0700 Subject: [PATCH 008/416] Filter out deleted statuses in feed --- bookwyrm/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 2bc840c0..f020d287 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -117,7 +117,7 @@ def get_activity_feed(user, filter_level, model=models.Status): activities = model if hasattr(model, 'objects'): - activities = model.objects + activities = model.objects.filter(deleted=False) activities = activities.order_by( '-created_date' From 0d614c7ebb394c5233a57223796ce51fb1b23309 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Oct 2020 12:38:06 -0700 Subject: [PATCH 009/416] Don't show deleted statuses --- bookwyrm/templates/snippets/status.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bookwyrm/templates/snippets/status.html b/bookwyrm/templates/snippets/status.html index 809cbe9e..5c570e0d 100644 --- a/bookwyrm/templates/snippets/status.html +++ b/bookwyrm/templates/snippets/status.html @@ -1,6 +1,7 @@ {% load humanize %} {% load fr_display %} +{% if not status.deleted %}
{% include 'snippets/status_header.html' with status=status %} @@ -40,3 +41,14 @@
+{% else %} +
+
+

+ {% include 'snippets/avatar.html' with user=status.user %} + {% include 'snippets/username.html' with user=status.user %} + deleted this status +

+
+
+{% endif %} From 10a0a6ac3776fb34015a3235ba61fcea97a37a2d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Oct 2020 12:40:47 -0700 Subject: [PATCH 010/416] hide deleted statuses from threads --- bookwyrm/templatetags/fr_display.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py index 818eae2a..cb4ee419 100644 --- a/bookwyrm/templatetags/fr_display.py +++ b/bookwyrm/templatetags/fr_display.py @@ -42,7 +42,8 @@ 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 + reply_parent=status, + deleted=False, ).select_subclasses().all()[:10] From a6d436d05d8299e9f63c188f6abacbf9c2be2095 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 13 Oct 2020 16:20:04 -0700 Subject: [PATCH 011/416] Fixes avatar in top bar on user page --- bookwyrm/templates/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index f14b76aa..f5f72e65 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -60,7 +60,7 @@ {% if request.user.is_authenticated %} From cedc79a962775b8e4fa150f6586e5d8a3ff60f10 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 14 Oct 2020 17:29:43 -0700 Subject: [PATCH 021/416] Tweaks handle_follow behavior for unknown users --- bookwyrm/incoming.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 0241fefb..b223ab16 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -112,9 +112,16 @@ def has_valid_signature(request, activity): def handle_follow(activity): ''' someone wants to follow a local user ''' # figure out who they want to follow -- not using get_or_create because - # we only allow you to follow local users - to_follow = models.User.objects.get(remote_id=activity['object']) - # raises models.User.DoesNotExist id the remote id is not found + # we only care if you want to follow local users + try: + to_follow = models.User.objects.get(remote_id=activity['object']) + except models.User.DoesNotExist: + # some rando, who cares + return + if not to_follow.local: + # just ignore follow alerts about other servers. maybe they should be + # handled. maybe they shouldn't be sent at all. + return # figure out who the actor is user = get_or_create_remote_user(activity['actor']) From e8ef8f710180e2cdf10075a23f57193f3dd9d916 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 15 Oct 2020 10:55:04 -0700 Subject: [PATCH 022/416] Fixes data encoding for signing tests --- bookwyrm/tests/test_signing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py index 7373c76f..62870336 100644 --- a/bookwyrm/tests/test_signing.py +++ b/bookwyrm/tests/test_signing.py @@ -61,7 +61,7 @@ class Signature(TestCase): digest=None, date=None): now = date or http_date() - data = get_follow_data(sender, self.rat) + data = json.dumps(get_follow_data(sender, self.rat)).encode('utf-8') digest = digest or make_digest(data) signature = make_signature( signer or sender, self.rat.inbox, now, digest) From db18014325f13205d2ceda58acdd81c936058508 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 15 Oct 2020 17:32:24 -0700 Subject: [PATCH 023/416] Adds test for incoming follow request --- bookwyrm/incoming.py | 11 +++--- bookwyrm/tests/test_incoming_follow.py | 47 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 bookwyrm/tests/test_incoming_follow.py diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index b223ab16..5aa975b4 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -124,10 +124,10 @@ def handle_follow(activity): return # figure out who the actor is - user = get_or_create_remote_user(activity['actor']) + actor = get_or_create_remote_user(activity['actor']) try: relationship = models.UserFollowRequest.objects.create( - user_subject=user, + user_subject=actor, user_object=to_follow, relationship_id=activity['id'] ) @@ -143,14 +143,15 @@ def handle_follow(activity): status_builder.create_notification( to_follow, 'FOLLOW', - related_user=user + related_user=actor ) - outgoing.handle_accept(user, to_follow, relationship) + outgoing.handle_accept(actor, to_follow, relationship) else: + # Accept will be triggered manually status_builder.create_notification( to_follow, 'FOLLOW_REQUEST', - related_user=user + related_user=actor ) diff --git a/bookwyrm/tests/test_incoming_follow.py b/bookwyrm/tests/test_incoming_follow.py new file mode 100644 index 00000000..a3ac3ebe --- /dev/null +++ b/bookwyrm/tests/test_incoming_follow.py @@ -0,0 +1,47 @@ +import json +from django.test import TestCase + +from bookwyrm import models, incoming + + +class Follow(TestCase): + ''' not too much going on in the books model but here we are ''' + def setUp(self): + self.remote_user = models.User.objects.create_user( + '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', + ) + self.local_user = models.User.objects.create_user( + 'mouse', 'mouse@mouse.com', 'mouseword') + self.local_user.remote_id = 'http://local.com/user/mouse' + self.local_user.save() + + + def test_handle_follow(self): + 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": "http://local.com/user/mouse" + } + + incoming.handle_follow(activity) + + # notification created + notification = models.Notification.objects.get() + self.assertEqual(notification.user, self.local_user) + self.assertEqual(notification.notification_type, 'FOLLOW') + + # the request should have been deleted + requests = models.UserFollowRequest.objects.all() + self.assertEqual(list(requests), []) + + # the follow relationship should exist + follow = models.UserFollows.objects.get(user_object=self.local_user) + self.assertEqual(follow.user_subject, self.remote_user) + + # an Accept should be sent out From 2d2863d4a8b202076077eb57261c7e4cf2c24b8a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 15 Oct 2020 17:46:23 -0700 Subject: [PATCH 024/416] Adds more incoming follow test cases --- bookwyrm/tests/test_incoming_follow.py | 49 +++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/bookwyrm/tests/test_incoming_follow.py b/bookwyrm/tests/test_incoming_follow.py index a3ac3ebe..b5fbec00 100644 --- a/bookwyrm/tests/test_incoming_follow.py +++ b/bookwyrm/tests/test_incoming_follow.py @@ -44,4 +44,51 @@ class Follow(TestCase): follow = models.UserFollows.objects.get(user_object=self.local_user) self.assertEqual(follow.user_subject, self.remote_user) - # an Accept should be sent out + + def test_handle_follow_manually_approved(self): + 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": "http://local.com/user/mouse" + } + + self.local_user.manually_approves_followers = True + self.local_user.save() + + incoming.handle_follow(activity) + + # notification created + notification = models.Notification.objects.get() + self.assertEqual(notification.user, self.local_user) + self.assertEqual(notification.notification_type, 'FOLLOW_REQUEST') + + # the request should exist + request = models.UserFollowRequest.objects.get() + self.assertEqual(request.user_subject, self.remote_user) + self.assertEqual(request.user_object, self.local_user) + + # the follow relationship should not exist + follow = models.UserFollows.objects.all() + self.assertEqual(list(follow), []) + + + def test_nonexistent_user_follow(self): + 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": "http://local.com/user/nonexistent-user" + } + + incoming.handle_follow(activity) + + # do nothing + notifications = models.Notification.objects.all() + self.assertEqual(list(notifications), []) + requests = models.UserFollowRequest.objects.all() + self.assertEqual(list(requests), []) + follows = models.UserFollows.objects.all() + self.assertEqual(list(follows), []) From 7a01d284c681f16cb3dc825cb6cb16a5b2600764 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 09:23:14 -0700 Subject: [PATCH 025/416] Incoming follow accept test --- bookwyrm/tests/test_incoming_follow_accept.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 bookwyrm/tests/test_incoming_follow_accept.py diff --git a/bookwyrm/tests/test_incoming_follow_accept.py b/bookwyrm/tests/test_incoming_follow_accept.py new file mode 100644 index 00000000..c30dc248 --- /dev/null +++ b/bookwyrm/tests/test_incoming_follow_accept.py @@ -0,0 +1,51 @@ +import json +from django.test import TestCase + +from bookwyrm import models, incoming + + +class IncomingFollowAccept(TestCase): + ''' not too much going on in the books model but here we are ''' + def setUp(self): + self.remote_user = models.User.objects.create_user( + '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', + ) + self.local_user = models.User.objects.create_user( + 'mouse', 'mouse@mouse.com', 'mouseword') + self.local_user.remote_id = 'http://local.com/user/mouse' + self.local_user.save() + + + def test_handle_follow_accept(self): + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123#accepts", + "type": "Accept", + "actor": "https://example.com/users/rat", + "object": { + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "http://local.com/user/mouse", + "object": "https://example.com/users/rat" + } + } + + models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + self.assertEqual(models.UserFollowRequest.objects.count(), 1) + + incoming.handle_follow_accept(activity) + + # request should be deleted + self.assertEqual(models.UserFollowRequest.objects.count(), 0) + + # relationship should be created + follows = self.remote_user.followers + self.assertEqual(follows.count(), 1) + self.assertEqual(follows.first(), self.local_user) From 7a153e185a798f491cb315878393de87ae77df46 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 09:45:14 -0700 Subject: [PATCH 026/416] User activitypub tests --- bookwyrm/tests/activitypub/test_person.py | 9 ---- bookwyrm/tests/models/test_user_model.py | 51 +++++++++++++++++------ 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/bookwyrm/tests/activitypub/test_person.py b/bookwyrm/tests/activitypub/test_person.py index 8a077a29..bec9e19b 100644 --- a/bookwyrm/tests/activitypub/test_person.py +++ b/bookwyrm/tests/activitypub/test_person.py @@ -21,12 +21,3 @@ class Person(TestCase): self.assertEqual(activity.id, 'https://example.com/user/mouse') self.assertEqual(activity.preferredUsername, 'mouse') self.assertEqual(activity.type, 'Person') - - - def test_serialize_model(self): - activity = self.user.to_activity() - self.assertEqual(activity['id'], self.user.remote_id) - self.assertEqual( - activity['endpoints'], - {'sharedInbox': self.user.shared_inbox} - ) diff --git a/bookwyrm/tests/models/test_user_model.py b/bookwyrm/tests/models/test_user_model.py index a423de37..0b43cc00 100644 --- a/bookwyrm/tests/models/test_user_model.py +++ b/bookwyrm/tests/models/test_user_model.py @@ -7,28 +7,55 @@ from bookwyrm.settings import DOMAIN class User(TestCase): def setUp(self): - models.User.objects.create_user( + self.user = models.User.objects.create_user( 'mouse', 'mouse@mouse.mouse', 'mouseword') def test_computed_fields(self): ''' username instead of id here ''' - user = models.User.objects.get(localname='mouse') expected_id = 'https://%s/user/mouse' % DOMAIN - self.assertEqual(user.remote_id, expected_id) - self.assertEqual(user.username, 'mouse@%s' % DOMAIN) - self.assertEqual(user.localname, 'mouse') - self.assertEqual(user.shared_inbox, 'https://%s/inbox' % DOMAIN) - self.assertEqual(user.inbox, '%s/inbox' % expected_id) - self.assertEqual(user.outbox, '%s/outbox' % expected_id) - self.assertIsNotNone(user.private_key) - self.assertIsNotNone(user.public_key) + 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.assertIsNotNone(self.user.private_key) + self.assertIsNotNone(self.user.public_key) def test_user_shelves(self): - user = models.User.objects.get(localname='mouse') - shelves = models.Shelf.objects.filter(user=user).all() + shelves = models.Shelf.objects.filter(user=self.user).all() self.assertEqual(len(shelves), 3) names = [s.name for s in shelves] self.assertEqual(names, ['To Read', 'Currently Reading', 'Read']) ids = [s.identifier for s in shelves] self.assertEqual(ids, ['to-read', 'reading', 'read']) + + + 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['followers'], self.user.ap_followers) + 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) From 2a0af0138dcdf1e9631c37f7a822fa005548e12b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 10:37:33 -0700 Subject: [PATCH 027/416] Uses activitypub mixin in relationship models plus tests --- bookwyrm/incoming.py | 2 +- .../migrations/0054_auto_20201016_1707.py | 25 ++++ bookwyrm/models/relationship.py | 42 +++--- .../tests/models/test_relationship_models.py | 120 ++++++++++++++++++ 4 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 bookwyrm/migrations/0054_auto_20201016_1707.py create mode 100644 bookwyrm/tests/models/test_relationship_models.py diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index b223ab16..c9e88dbb 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -129,7 +129,7 @@ def handle_follow(activity): relationship = models.UserFollowRequest.objects.create( user_subject=user, user_object=to_follow, - relationship_id=activity['id'] + remote_id=activity['id'] ) except django.db.utils.IntegrityError as err: if err.__cause__.diag.constraint_name != 'userfollowrequest_unique': diff --git a/bookwyrm/migrations/0054_auto_20201016_1707.py b/bookwyrm/migrations/0054_auto_20201016_1707.py new file mode 100644 index 00000000..043ff12d --- /dev/null +++ b/bookwyrm/migrations/0054_auto_20201016_1707.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.7 on 2020-10-16 17:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0053_auto_20201014_1700'), + ] + + operations = [ + migrations.RemoveField( + model_name='userblocks', + name='relationship_id', + ), + migrations.RemoveField( + model_name='userfollowrequest', + name='relationship_id', + ), + migrations.RemoveField( + model_name='userfollows', + name='relationship_id', + ), + ] diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index e357955e..e86b9098 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -2,10 +2,10 @@ from django.db import models from bookwyrm import activitypub -from .base_model import BookWyrmModel +from .base_model import ActivitypubMixin, ActivityMapping, BookWyrmModel -class UserRelationship(BookWyrmModel): +class UserRelationship(ActivitypubMixin, BookWyrmModel): ''' many-to-many through table for followers ''' user_subject = models.ForeignKey( 'User', @@ -17,8 +17,6 @@ class UserRelationship(BookWyrmModel): on_delete=models.PROTECT, related_name='%(class)s_user_object' ) - # follow or follow_request for pending TODO: blocking? - relationship_id = models.CharField(max_length=100) class Meta: ''' relationships should be unique ''' @@ -34,25 +32,35 @@ class UserRelationship(BookWyrmModel): ) ] - def get_remote_id(self): + activity_mappings = [ + ActivityMapping('id', 'remote_id'), + ActivityMapping('actor', 'user_subject'), + ActivityMapping('object', 'user_object'), + ] + activity_serializer = activitypub.Follow + + def get_remote_id(self, status=None): ''' use shelf identifier in remote_id ''' + status = status or 'follows' base_path = self.user_subject.remote_id - return '%s#%s/%d' % (base_path, self.status, self.id) + return '%s#%s/%d' % (base_path, status, self.id) + def to_accept_activity(self): ''' generate an Accept for this follow request ''' return activitypub.Accept( - id='%s#accepts/follows/' % self.remote_id, - actor=self.user_subject.remote_id, - object=self.user_object.remote_id, + id=self.get_remote_id(status='accepts'), + actor=self.user_object.remote_id, + object=self.to_activity() ).serialize() + def to_reject_activity(self): ''' generate an Accept for this follow request ''' return activitypub.Reject( - id='%s#rejects/follows/' % self.remote_id, - actor=self.user_subject.remote_id, - object=self.user_object.remote_id, + id=self.get_remote_id(status='rejects'), + actor=self.user_object.remote_id, + object=self.to_activity() ).serialize() @@ -66,7 +74,7 @@ class UserFollows(UserRelationship): return cls( user_subject=follow_request.user_subject, user_object=follow_request.user_object, - relationship_id=follow_request.relationship_id, + remote_id=follow_request.remote_id, ) @@ -74,14 +82,6 @@ class UserFollowRequest(UserRelationship): ''' following a user requires manual or automatic confirmation ''' status = 'follow_request' - def to_activity(self): - ''' request activity ''' - return activitypub.Follow( - id=self.remote_id, - actor=self.user_subject.remote_id, - object=self.user_object.remote_id, - ).serialize() - class UserBlocks(UserRelationship): ''' prevent another user from following you and seeing your posts ''' diff --git a/bookwyrm/tests/models/test_relationship_models.py b/bookwyrm/tests/models/test_relationship_models.py new file mode 100644 index 00000000..1e763f59 --- /dev/null +++ b/bookwyrm/tests/models/test_relationship_models.py @@ -0,0 +1,120 @@ +''' testing models ''' +from django.test import TestCase + +from bookwyrm import models + + +class Relationship(TestCase): + def setUp(self): + self.remote_user = models.User.objects.create_user( + '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', + ) + self.local_user = models.User.objects.create_user( + 'mouse', 'mouse@mouse.com', 'mouseword') + self.local_user.remote_id = 'http://local.com/user/mouse' + self.local_user.save() + + def test_user_follows(self): + rel = models.UserFollows.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + + self.assertEqual( + rel.remote_id, + 'http://local.com/user/mouse#follows/%d' % rel.id + ) + + activity = rel.to_activity() + self.assertEqual(activity['id'], rel.remote_id) + self.assertEqual(activity['actor'], self.local_user.remote_id) + self.assertEqual(activity['object'], self.remote_user.remote_id) + + def test_user_follow_accept_serialization(self): + rel = models.UserFollows.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + + self.assertEqual( + rel.remote_id, + 'http://local.com/user/mouse#follows/%d' % rel.id + ) + accept = rel.to_accept_activity() + self.assertEqual(accept['type'], 'Accept') + self.assertEqual( + accept['id'], + 'http://local.com/user/mouse#accepts/%d' % rel.id + ) + self.assertEqual(accept['actor'], self.remote_user.remote_id) + self.assertEqual(accept['object']['id'], rel.remote_id) + self.assertEqual(accept['object']['actor'], self.local_user.remote_id) + self.assertEqual(accept['object']['object'], self.remote_user.remote_id) + + def test_user_follow_reject_serialization(self): + rel = models.UserFollows.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + + self.assertEqual( + rel.remote_id, + 'http://local.com/user/mouse#follows/%d' % rel.id + ) + reject = rel.to_reject_activity() + self.assertEqual(reject['type'], 'Reject') + self.assertEqual( + reject['id'], + 'http://local.com/user/mouse#rejects/%d' % rel.id + ) + self.assertEqual(reject['actor'], self.remote_user.remote_id) + self.assertEqual(reject['object']['id'], rel.remote_id) + self.assertEqual(reject['object']['actor'], self.local_user.remote_id) + self.assertEqual(reject['object']['object'], self.remote_user.remote_id) + + + def test_user_follows_from_request(self): + request = models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + self.assertEqual( + request.remote_id, + 'http://local.com/user/mouse#follows/%d' % request.id + ) + 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 + ) + self.assertEqual(rel.status, 'follows') + self.assertEqual(rel.user_subject, self.local_user) + self.assertEqual(rel.user_object, self.remote_user) + + + def test_user_follows_from_request_custom_remote_id(self): + request = models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user, + remote_id='http://antoher.server/sdkfhskdjf/23' + ) + 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.user_subject, self.local_user) + self.assertEqual(rel.user_object, self.remote_user) From b640e6651b34af4fc4c9cfc8ed820d8b659ff077 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 10:52:07 -0700 Subject: [PATCH 028/416] Narrow scope of test coverage reporting --- fr-dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fr-dev b/fr-dev index ae40e889..56494473 100755 --- a/fr-dev +++ b/fr-dev @@ -39,7 +39,7 @@ case "$1" in ;; test) shift 1 - docker-compose exec web coverage run --source='.' manage.py test "$@" + docker-compose exec web coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@" ;; test_report) docker-compose exec web coverage report From b32fce25d9260eeee443aed37ed68761849ff02f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 12:24:29 -0700 Subject: [PATCH 029/416] tweaks follow handling --- bookwyrm/incoming.py | 6 ++---- bookwyrm/outgoing.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 04b7270a..d348e43a 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -134,10 +134,8 @@ def handle_follow(activity): except django.db.utils.IntegrityError as err: if err.__cause__.diag.constraint_name != 'userfollowrequest_unique': raise - # Duplicate follow request. Not sure what the correct behaviour is, but - # just dropping it works for now. We should perhaps generate the - # Accept, but then do we need to match the activity id? - return + relationship = models.UserFollowRequest.objects.get(remote_id=activity['id']) + # send the accept normally for a duplicate request if not to_follow.manually_approves_followers: status_builder.create_notification( diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 0efa8a4a..2c4c6def 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -66,7 +66,7 @@ def handle_follow(user, to_follow): user_object=to_follow, ) activity = relationship.to_activity() - broadcast(user, activity, direct_recipients=[to_follow]) + broadcast(user, activity, privacy='direct', direct_recipients=[to_follow]) def handle_unfollow(user, to_unfollow): @@ -76,7 +76,7 @@ def handle_unfollow(user, to_unfollow): user_object=to_unfollow ) activity = relationship.to_undo_activity(user) - broadcast(user, activity, direct_recipients=[to_unfollow]) + broadcast(user, activity, privacy='direct', direct_recipients=[to_unfollow]) to_unfollow.followers.remove(user) From b8040cd0dc64d66f3ea32da58ee33612d579a1f3 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 13:02:58 -0700 Subject: [PATCH 030/416] Move prod config files to prod branch --- docker-compose.yml | 5 +-- nginx/default.conf | 43 ------------------------ prod-docker-compose.yml | 74 ----------------------------------------- 3 files changed, 1 insertion(+), 121 deletions(-) delete mode 100644 prod-docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml index d7c4ec3b..f5391d42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,6 @@ services: - media_volume:/app/images db: image: postgres - env_file: .env volumes: - pgdata:/var/lib/postgresql/data networks: @@ -36,14 +35,12 @@ services: - 8000:8000 redis: image: redis - env_file: .env ports: - - "6379:6379" + - 6379:6379 networks: - main restart: on-failure celery_worker: - env_file: .env build: . networks: - main diff --git a/nginx/default.conf b/nginx/default.conf index 51165243..d3898287 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -20,46 +20,3 @@ server { alias /app/static/; } } - -# PROD version -# -#server { -# listen [::]:80; -# listen 80; -# -# server_name your-domain.com www.your-domain.com; -# -# location ~ /.well-known/acme-challenge { -# allow all; -# root /var/www/certbot; -# } -# -# # redirect http to https www -# return 301 https://www.your-domain.com$request_uri; -#} -# -#server { -# listen [::]:443 ssl http2; -# listen 443 ssl http2; -# -# server_name your-domain.com; -# -# # SSL code -# ssl_certificate /etc/nginx/ssl/live/your-domain.com/fullchain.pem; -# ssl_certificate_key /etc/nginx/ssl/live/your-domain.com/privkey.pem; -# -# location / { -# proxy_pass http://web; -# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -# proxy_set_header Host $host; -# proxy_redirect off; -# } -# -# location /images/ { -# alias /app/images/; -# } -# -# location /static/ { -# alias /app/static/; -# } -#} diff --git a/prod-docker-compose.yml b/prod-docker-compose.yml deleted file mode 100644 index 0ace0df0..00000000 --- a/prod-docker-compose.yml +++ /dev/null @@ -1,74 +0,0 @@ -version: '3' - -services: - nginx: - image: nginx:latest - ports: - - 80:80 - - 443:443 - depends_on: - - web - networks: - - main - volumes: - - ./nginx:/etc/nginx/conf.d - - ./certbot/conf:/etc/nginx/ssl - - ./certbot/data:/var/www/certbot - - static_volume:/app/static - - media_volume:/app/images - certbot: - image: certbot/certbot:latest - command: certonly --webroot --webroot-path=/var/www/certbot --email your-email@domain.com --agree-tos --no-eff-email -d your-domain.com -d www.your-domain.com - volumes: - - ./certbot/conf:/etc/letsencrypt - - ./certbot/logs:/var/log/letsencrypt - - ./certbot/data:/var/www/certbot - db: - image: postgres - env_file: .env - volumes: - - pgdata:/var/lib/postgresql/data - networks: - - main - web: - build: . - command: python manage.py runserver 0.0.0.0:8000 - volumes: - - .:/app - - static_volume:/app/static - - media_volume:/app/images - depends_on: - - db - - celery_worker - networks: - - main - ports: - - 8000:8000 - redis: - image: redis - env_file: .env - ports: - - "6379:6379" - networks: - - main - restart: on-failure - celery_worker: - env_file: .env - build: . - networks: - - main - command: celery -A celerywyrm worker -l info - volumes: - - .:/app - - static_volume:/app/static - - media_volume:/app/images - depends_on: - - db - - redis - restart: on-failure -volumes: - pgdata: - static_volume: - media_volume: -networks: - main: From cae7bbf834dd67b55f00469fb385ee62afcc1b75 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 13:20:12 -0700 Subject: [PATCH 031/416] oh apparently I DID need to explicitly name .env --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index f5391d42..29ec83ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - media_volume:/app/images db: image: postgres + env_file: .env volumes: - pgdata:/var/lib/postgresql/data networks: @@ -35,12 +36,14 @@ services: - 8000:8000 redis: image: redis + env_file: .env ports: - 6379:6379 networks: - main restart: on-failure celery_worker: + env_file: .env build: . networks: - main From 694de44f3f0eeb0860d3c1706a73ff11efb1ca6e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 14:04:06 -0700 Subject: [PATCH 032/416] reorganize incoming/outgoing tests --- bookwyrm/tests/incoming/__init__.py | 1 + .../test_favorite.py} | 3 +- .../test_follow.py} | 4 +- .../test_follow_accept.py} | 2 - bookwyrm/tests/outgoing/__init__.py | 1 + bookwyrm/tests/outgoing/test_follow.py | 37 +++++++++++++++++++ 6 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 bookwyrm/tests/incoming/__init__.py rename bookwyrm/tests/{test_incoming_favorite.py => incoming/test_favorite.py} (95%) rename bookwyrm/tests/{test_incoming_follow.py => incoming/test_follow.py} (96%) rename bookwyrm/tests/{test_incoming_follow_accept.py => incoming/test_follow_accept.py} (95%) create mode 100644 bookwyrm/tests/outgoing/__init__.py create mode 100644 bookwyrm/tests/outgoing/test_follow.py diff --git a/bookwyrm/tests/incoming/__init__.py b/bookwyrm/tests/incoming/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/bookwyrm/tests/incoming/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/bookwyrm/tests/test_incoming_favorite.py b/bookwyrm/tests/incoming/test_favorite.py similarity index 95% rename from bookwyrm/tests/test_incoming_favorite.py rename to bookwyrm/tests/incoming/test_favorite.py index 03502145..eeba9000 100644 --- a/bookwyrm/tests/test_incoming_favorite.py +++ b/bookwyrm/tests/incoming/test_favorite.py @@ -6,7 +6,6 @@ from bookwyrm import models, incoming class Favorite(TestCase): - ''' not too much going on in the books model but here we are ''' def setUp(self): self.remote_user = models.User.objects.create_user( 'rat', 'rat@rat.com', 'ratword', @@ -25,7 +24,7 @@ class Favorite(TestCase): ) datafile = pathlib.Path(__file__).parent.joinpath( - 'data/ap_user.json' + '../data/ap_user.json' ) self.user_data = json.loads(datafile.read_bytes()) diff --git a/bookwyrm/tests/test_incoming_follow.py b/bookwyrm/tests/incoming/test_follow.py similarity index 96% rename from bookwyrm/tests/test_incoming_follow.py rename to bookwyrm/tests/incoming/test_follow.py index b5fbec00..51ab3c43 100644 --- a/bookwyrm/tests/test_incoming_follow.py +++ b/bookwyrm/tests/incoming/test_follow.py @@ -1,11 +1,9 @@ -import json from django.test import TestCase from bookwyrm import models, incoming -class Follow(TestCase): - ''' not too much going on in the books model but here we are ''' +class IncomingFollow(TestCase): def setUp(self): self.remote_user = models.User.objects.create_user( 'rat', 'rat@rat.com', 'ratword', diff --git a/bookwyrm/tests/test_incoming_follow_accept.py b/bookwyrm/tests/incoming/test_follow_accept.py similarity index 95% rename from bookwyrm/tests/test_incoming_follow_accept.py rename to bookwyrm/tests/incoming/test_follow_accept.py index c30dc248..ba88bb40 100644 --- a/bookwyrm/tests/test_incoming_follow_accept.py +++ b/bookwyrm/tests/incoming/test_follow_accept.py @@ -1,11 +1,9 @@ -import json from django.test import TestCase from bookwyrm import models, incoming class IncomingFollowAccept(TestCase): - ''' not too much going on in the books model but here we are ''' def setUp(self): self.remote_user = models.User.objects.create_user( 'rat', 'rat@rat.com', 'ratword', diff --git a/bookwyrm/tests/outgoing/__init__.py b/bookwyrm/tests/outgoing/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/bookwyrm/tests/outgoing/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/bookwyrm/tests/outgoing/test_follow.py b/bookwyrm/tests/outgoing/test_follow.py new file mode 100644 index 00000000..7932d9c1 --- /dev/null +++ b/bookwyrm/tests/outgoing/test_follow.py @@ -0,0 +1,37 @@ +from django.test import TestCase + +from bookwyrm import models, outgoing + + +class OutgoingFollow(TestCase): + def setUp(self): + self.remote_user = models.User.objects.create_user( + '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', + ) + self.local_user = models.User.objects.create_user( + 'mouse', 'mouse@mouse.com', 'mouseword', + local=True, + remote_id='http://local.com/users/mouse', + ) + + + def test_handle_follow(self): + self.assertEqual(models.UserFollowRequest.objects.count(), 0) + + outgoing.handle_follow(self.local_user, self.remote_user) + 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') + + def test_handle_unfollow(self): + self.remote_user.followers.add(self.local_user) + self.assertEqual(self.remote_user.followers.count(), 1) + outgoing.handle_unfollow(self.local_user, self.remote_user) + + self.assertEqual(self.remote_user.followers.count(), 0) From a567bd4e613c6aa7d8c692ba1e271c9207cc0126 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 14:14:07 -0700 Subject: [PATCH 033/416] Simplifies outgoing follow logic --- bookwyrm/incoming.py | 2 +- bookwyrm/outgoing.py | 4 +++- bookwyrm/tests/outgoing/test_follow.py | 19 ++++++++++++++++++- bookwyrm/view_actions.py | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index d348e43a..57ed0220 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -143,7 +143,7 @@ def handle_follow(activity): 'FOLLOW', related_user=actor ) - outgoing.handle_accept(actor, to_follow, relationship) + outgoing.handle_accept(relationship) else: # Accept will be triggered manually status_builder.create_notification( diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 2c4c6def..0a09a101 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -80,8 +80,10 @@ def handle_unfollow(user, to_unfollow): to_unfollow.followers.remove(user) -def handle_accept(user, to_follow, follow_request): +def handle_accept(follow_request): ''' send an acceptance message to a follow request ''' + user = follow_request.user_subject + to_follow = follow_request.user_object with transaction.atomic(): relationship = models.UserFollows.from_request(follow_request) follow_request.delete() diff --git a/bookwyrm/tests/outgoing/test_follow.py b/bookwyrm/tests/outgoing/test_follow.py index 7932d9c1..4ecf3a91 100644 --- a/bookwyrm/tests/outgoing/test_follow.py +++ b/bookwyrm/tests/outgoing/test_follow.py @@ -3,7 +3,7 @@ from django.test import TestCase from bookwyrm import models, outgoing -class OutgoingFollow(TestCase): +class Following(TestCase): def setUp(self): self.remote_user = models.User.objects.create_user( 'rat', 'rat@rat.com', 'ratword', @@ -29,9 +29,26 @@ class OutgoingFollow(TestCase): self.assertEqual(rel.user_object, self.remote_user) self.assertEqual(rel.status, 'follow_request') + def test_handle_unfollow(self): self.remote_user.followers.add(self.local_user) self.assertEqual(self.remote_user.followers.count(), 1) outgoing.handle_unfollow(self.local_user, self.remote_user) self.assertEqual(self.remote_user.followers.count(), 0) + + + def test_handle_accept(self): + rel = models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + rel_id = rel.id + + outgoing.handle_accept(rel) + # request should be deleted + self.assertEqual( + models.UserFollowRequest.objects.filter(id=rel_id).count(), 0 + ) + # follow relationship should exist + self.assertEqual(self.remote_user.followers.first(), self.local_user) diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 992a270d..a4814ff0 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -473,7 +473,7 @@ def accept_follow_request(request): # Request already dealt with. pass else: - outgoing.handle_accept(requester, request.user, follow_request) + outgoing.handle_accept(follow_request) return redirect('/user/%s' % request.user.localname) From 75c695b3c60e305bbd7b9fd58c1e70f592b1b83d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 14:28:25 -0700 Subject: [PATCH 034/416] Updates and tests outgoing reject --- bookwyrm/incoming.py | 6 ++++-- bookwyrm/outgoing.py | 8 +++++--- bookwyrm/tests/outgoing/test_follow.py | 18 ++++++++++++++++++ bookwyrm/view_actions.py | 2 +- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 57ed0220..bbce14c1 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -134,7 +134,9 @@ def handle_follow(activity): except django.db.utils.IntegrityError as err: if err.__cause__.diag.constraint_name != 'userfollowrequest_unique': raise - relationship = models.UserFollowRequest.objects.get(remote_id=activity['id']) + relationship = models.UserFollowRequest.objects.get( + remote_id=activity['id'] + ) # send the accept normally for a duplicate request if not to_follow.manually_approves_followers: @@ -194,7 +196,7 @@ def handle_follow_reject(activity): user_object=rejecter ) request.delete() - #raises models.UserFollowRequest.DoesNotExist: + #raises models.UserFollowRequest.DoesNotExist @app.task diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 0a09a101..8775712a 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -93,10 +93,12 @@ def handle_accept(follow_request): broadcast(to_follow, activity, privacy='direct', direct_recipients=[user]) -def handle_reject(user, to_follow, relationship): +def handle_reject(follow_request): ''' a local user who managed follows rejects a follow request ''' - activity = relationship.to_reject_activity(user) - relationship.delete() + user = follow_request.user_subject + to_follow = follow_request.user_object + activity = follow_request.to_reject_activity() + follow_request.delete() broadcast(to_follow, activity, privacy='direct', direct_recipients=[user]) diff --git a/bookwyrm/tests/outgoing/test_follow.py b/bookwyrm/tests/outgoing/test_follow.py index 4ecf3a91..82a476f6 100644 --- a/bookwyrm/tests/outgoing/test_follow.py +++ b/bookwyrm/tests/outgoing/test_follow.py @@ -52,3 +52,21 @@ class Following(TestCase): ) # follow relationship should exist self.assertEqual(self.remote_user.followers.first(), self.local_user) + + + def test_handle_reject(self): + rel = models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + rel_id = rel.id + + outgoing.handle_reject(rel) + # request should be deleted + 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 + ) diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index a4814ff0..b8436157 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -495,7 +495,7 @@ def delete_follow_request(request): except models.UserFollowRequest.DoesNotExist: return HttpResponseBadRequest() - outgoing.handle_reject(requester, request.user, follow_request) + outgoing.handle_reject(follow_request) return redirect('/user/%s' % request.user.localname) From 4f07a567bd5386c0eac9c32cacd0330d1cbaa2a5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 15:07:41 -0700 Subject: [PATCH 035/416] Shelving tests --- bookwyrm/outgoing.py | 15 ++-- bookwyrm/tests/outgoing/test_shelving.py | 97 ++++++++++++++++++++++++ bookwyrm/tests/status/test_quotation.py | 4 +- 3 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 bookwyrm/tests/outgoing/test_shelving.py diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 8775712a..2db667d7 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -111,11 +111,16 @@ def handle_shelve(user, book, shelf): broadcast(user, shelve.to_add_activity(user)) # tell the world about this cool thing that happened - message = { - 'to-read': 'wants to read', - 'reading': 'started reading', - 'read': 'finished reading' - }[shelf.identifier] + try: + message = { + '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]) status.save() diff --git a/bookwyrm/tests/outgoing/test_shelving.py b/bookwyrm/tests/outgoing/test_shelving.py new file mode 100644 index 00000000..acf816e1 --- /dev/null +++ b/bookwyrm/tests/outgoing/test_shelving.py @@ -0,0 +1,97 @@ +from django.test import TestCase + +from bookwyrm import models, outgoing + + +class Shelving(TestCase): + def setUp(self): + self.user = models.User.objects.create_user( + 'mouse', 'mouse@mouse.com', 'mouseword', + local=True, + remote_id='http://local.com/users/mouse', + ) + self.book = models.Edition.objects.create( + title='Example Edition', + remote_id='https://example.com/book/1', + ) + self.shelf = models.Shelf.objects.create( + name='Test Shelf', + identifier='test-shelf', + user=self.user + ) + + + def test_handle_shelve(self): + outgoing.handle_shelve(self.user, self.book, self.shelf) + # make sure the book is on the shelf + self.assertEqual(self.shelf.books.get(), self.book) + + + def test_handle_shelve_to_read(self): + shelf = models.Shelf.objects.get(identifier='to-read') + + outgoing.handle_shelve(self.user, self.book, shelf) + # make sure the book is on the shelf + self.assertEqual(shelf.books.get(), self.book) + + # it should have posted a status about this + status = models.GeneratedStatus.objects.get() + self.assertEqual(status.content, 'wants to read') + self.assertEqual(status.user, self.user) + self.assertEqual(status.mention_books.count(), 1) + self.assertEqual(status.mention_books.first(), self.book) + + # and it should not create a read-through + self.assertEqual(models.ReadThrough.objects.count(), 0) + + + def test_handle_shelve_reading(self): + shelf = models.Shelf.objects.get(identifier='reading') + + outgoing.handle_shelve(self.user, self.book, shelf) + # make sure the book is on the shelf + self.assertEqual(shelf.books.get(), self.book) + + # it should have posted a status about this + status = models.GeneratedStatus.objects.order_by('-published_date').first() + self.assertEqual(status.content, 'started reading') + self.assertEqual(status.user, self.user) + self.assertEqual(status.mention_books.count(), 1) + self.assertEqual(status.mention_books.first(), self.book) + + # and it should create a read-through + readthrough = models.ReadThrough.objects.get() + self.assertEqual(readthrough.user, self.user) + self.assertEqual(readthrough.book.id, self.book.id) + self.assertIsNotNone(readthrough.start_date) + self.assertIsNone(readthrough.finish_date) + + + def test_handle_shelve_read(self): + shelf = models.Shelf.objects.get(identifier='read') + + outgoing.handle_shelve(self.user, self.book, shelf) + # make sure the book is on the shelf + self.assertEqual(shelf.books.get(), self.book) + + # it should have posted a status about this + status = models.GeneratedStatus.objects.order_by('-published_date').first() + self.assertEqual(status.content, 'finished reading') + self.assertEqual(status.user, self.user) + self.assertEqual(status.mention_books.count(), 1) + self.assertEqual(status.mention_books.first(), self.book) + + # and it should update the existing read-through + readthrough = models.ReadThrough.objects.get() + self.assertEqual(readthrough.user, self.user) + self.assertEqual(readthrough.book.id, self.book.id) + self.assertIsNotNone(readthrough.start_date) + self.assertIsNotNone(readthrough.finish_date) + + + def test_handle_unshelve(self): + self.shelf.books.add(self.book) + self.shelf.save() + self.assertEqual(self.shelf.books.count(), 1) + outgoing.handle_unshelve(self.user, self.book, self.shelf) + self.assertEqual(self.shelf.books.count(), 0) diff --git a/bookwyrm/tests/status/test_quotation.py b/bookwyrm/tests/status/test_quotation.py index 57755560..4892e21d 100644 --- a/bookwyrm/tests/status/test_quotation.py +++ b/bookwyrm/tests/status/test_quotation.py @@ -1,8 +1,6 @@ from django.test import TestCase -import json -import pathlib -from bookwyrm import activitypub, models +from bookwyrm import models from bookwyrm import status as status_builder From c0f51fa6aa45720a6b8da3d50d5466ccf6ef89f6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 15:40:23 -0700 Subject: [PATCH 036/416] Handle incomign update user activities --- bookwyrm/incoming.py | 16 ++++++++++- bookwyrm/tests/incoming/test_update_user.py | 30 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/tests/incoming/test_update_user.py diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index bbce14c1..dca5d12f 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -67,7 +67,7 @@ def shared_inbox(request): 'Like': handle_unfavorite, }, 'Update': { - 'Person': None,# TODO: handle_update_user + 'Person': handle_update_user, 'Document': handle_update_book, }, } @@ -293,6 +293,20 @@ def handle_tag(activity): status_builder.create_tag(user, book, activity['object']['name']) +@app.task +def handle_update_user(activity): + ''' receive an updated user Person activity object ''' + try: + user = models.User.objects.get(remote_id=activity['object']['id']) + except models.User.DoesNotExist: + # who is this person? who cares + return + activitypub.Person( + **activity['object'] + ).to_model(models.User, instance=user) + # model save() happens in the to_model function + + @app.task def handle_update_book(activity): ''' a remote instance changed a book (Document) ''' diff --git a/bookwyrm/tests/incoming/test_update_user.py b/bookwyrm/tests/incoming/test_update_user.py new file mode 100644 index 00000000..703078f1 --- /dev/null +++ b/bookwyrm/tests/incoming/test_update_user.py @@ -0,0 +1,30 @@ +import json +import pathlib +from django.test import TestCase + +from bookwyrm import models, incoming + + +class UpdateUser(TestCase): + def setUp(self): + self.user = models.User.objects.create_user( + 'mouse', 'mouse@mouse.com', 'mouseword', + remote_id='https://example.com/user/mouse', + local=False, + localname='mouse' + ) + + datafile = pathlib.Path(__file__).parent.joinpath( + '../data/ap_user.json' + ) + self.user_data = json.loads(datafile.read_bytes()) + + def test_handle_update_user(self): + self.assertIsNone(self.user.name) + self.assertEqual(self.user.localname, 'mouse') + + incoming.handle_update_user({'object': self.user_data}) + self.user = models.User.objects.get(id=self.user.id) + + self.assertEqual(self.user.name, 'MOUSE?? MOUSE!!') + self.assertEqual(self.user.localname, 'mouse') From 7f579ffefa94e38968788042c5e2df376d9d7787 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 17:00:10 -0700 Subject: [PATCH 037/416] Read incoming deletion activities --- bookwyrm/incoming.py | 15 +++++++++++++++ bookwyrm/migrations/0054_auto_20201016_2359.py | 18 ++++++++++++++++++ bookwyrm/models/status.py | 2 +- bookwyrm/status.py | 2 ++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/migrations/0054_auto_20201016_2359.py diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 54e2fb24..d5cfc36b 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -57,6 +57,7 @@ def shared_inbox(request): 'Accept': handle_follow_accept, 'Reject': handle_follow_reject, 'Create': handle_create, + 'Delete': handle_delete_status, 'Like': handle_favorite, 'Announce': handle_boost, 'Add': { @@ -229,6 +230,20 @@ def handle_create(activity): ) +@app.task +def handle_delete_status(activity): + ''' remove a status ''' + status_id = activity['object']['id'] + try: + status = models.Status.objects.select_subclasses().get( + remote_id=status_id + ) + except models.Status.DoesNotExist: + return + status_builder.delete_status(status) + + + @app.task def handle_favorite(activity): ''' approval of your good good post ''' diff --git a/bookwyrm/migrations/0054_auto_20201016_2359.py b/bookwyrm/migrations/0054_auto_20201016_2359.py new file mode 100644 index 00000000..c8ab3480 --- /dev/null +++ b/bookwyrm/migrations/0054_auto_20201016_2359.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-10-16 23:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0053_auto_20201006_2020'), + ] + + operations = [ + migrations.AlterField( + model_name='status', + name='deleted_date', + field=models.DateTimeField(), + ), + ] diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index f9f90467..0a70eb77 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -23,7 +23,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): # the created date can't be this, because of receiving federated posts published_date = models.DateTimeField(default=timezone.now) deleted = models.BooleanField(default=False) - deleted_date = models.DateTimeField(default=timezone.now) + deleted_date = models.DateTimeField() favorites = models.ManyToManyField( 'User', symmetrical=False, diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 190f5dd7..25619839 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -1,4 +1,5 @@ ''' Handle user activity ''' +from datetime import datetime from django.db import IntegrityError from bookwyrm import models @@ -9,6 +10,7 @@ from bookwyrm.sanitize_html import InputHtmlParser def delete_status(status): ''' replace the status with a tombstone ''' status.deleted = True + status.deleted_date = datetime.now() status.save() def create_rating(user, book, rating): From d1d339225cf35f2bf7bccf3d29383663bfb38632 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 17:11:17 -0700 Subject: [PATCH 038/416] Merge migrations --- bookwyrm/migrations/0055_merge_20201017_0011.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 bookwyrm/migrations/0055_merge_20201017_0011.py diff --git a/bookwyrm/migrations/0055_merge_20201017_0011.py b/bookwyrm/migrations/0055_merge_20201017_0011.py new file mode 100644 index 00000000..fd995eaa --- /dev/null +++ b/bookwyrm/migrations/0055_merge_20201017_0011.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.7 on 2020-10-17 00:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0054_auto_20201016_1707'), + ('bookwyrm', '0054_auto_20201016_2359'), + ] + + operations = [ + ] From 8cf7e4405d90f78bd9a4c8c2cc4233b610b27d66 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 16 Oct 2020 19:13:18 -0700 Subject: [PATCH 039/416] minor style fixes --- bookwyrm/activitypub/base_activity.py | 9 ++++----- bookwyrm/activitypub/book.py | 1 - bookwyrm/activitypub/note.py | 2 +- bookwyrm/broadcast.py | 3 --- bookwyrm/emailing.py | 1 - bookwyrm/urls.py | 9 ++++++++- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 042b8a14..1cffd0ed 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -44,10 +44,9 @@ class ActivityObject: type: str def __init__(self, **kwargs): - ''' 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 ''' + ''' 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 ''' for field in fields(self): try: value = kwargs[field.name] @@ -59,7 +58,7 @@ class ActivityObject: def to_model(self, model, instance=None): - ''' convert from an activity to a model ''' + ''' convert from an activity to a model instance ''' if not isinstance(self, model.activity_serializer): raise TypeError('Wrong activity type for model') diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 2a50dd6a..49f2deb8 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -52,7 +52,6 @@ class Work(Book): type: str = 'Work' - @dataclass(init=False) class Author(ActivityObject): ''' author of a book ''' diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 54730fb6..4e36b01f 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -6,6 +6,7 @@ from .base_activity import ActivityObject, Image @dataclass(init=False) class Tombstone(ActivityObject): + ''' the placeholder for a deleted status ''' url: str published: str deleted: str @@ -23,7 +24,6 @@ class Note(ActivityObject): cc: List[str] content: str replies: Dict - # TODO: this is wrong??? attachment: List[Image] = field(default=lambda: []) sensitive: bool = False type: str = 'Note' diff --git a/bookwyrm/broadcast.py b/bookwyrm/broadcast.py index 301fe84f..9b071b39 100644 --- a/bookwyrm/broadcast.py +++ b/bookwyrm/broadcast.py @@ -13,7 +13,6 @@ def get_public_recipients(user, software=None): ''' everybody and their public inboxes ''' followers = user.followers.filter(local=False) if software: - # TODO: eventually we may want to handle particular software differently followers = followers.filter(bookwyrm_user=(software == 'bookwyrm')) # we want shared inboxes when available @@ -36,7 +35,6 @@ def broadcast(sender, activity, software=None, \ # start with parsing the direct recipients recipients = [u.inbox for u in direct_recipients or []] # and then add any other recipients - # TODO: other kinds of privacy if privacy == 'public': recipients += get_public_recipients(sender, software=software) broadcast_task.delay( @@ -55,7 +53,6 @@ def broadcast_task(sender_id, activity, recipients): try: sign_and_send(sender, activity, recipient) except requests.exceptions.HTTPError as e: - # TODO: maybe keep track of users who cause errors errors.append({ 'error': str(e), 'recipient': recipient, diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 12dee65f..2319d467 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -6,7 +6,6 @@ from bookwyrm.tasks import app def password_reset_email(reset_code): ''' generate a password reset email ''' - # TODO; this should be tempalted site = models.SiteSettings.get() send_email.delay( reset_code.user.email, diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 331efee5..5d75f49b 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -11,7 +11,14 @@ localname_regex = r'(?P[\w\-_]+)' user_path = r'^user/%s' % username_regex local_user_path = r'^user/%s' % localname_regex -status_types = ['status', 'review', 'comment', 'quotation', 'boost', 'generatedstatus'] +status_types = [ + 'status', + 'review', + 'comment', + 'quotation', + 'boost', + 'generatedstatus' +] status_path = r'%s/(%s)/(?P\d+)' % \ (local_user_path, '|'.join(status_types)) From 1cc0c14f8627812701b06a8935f6154c4a38c364 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 20 Oct 2020 18:50:39 -0700 Subject: [PATCH 040/416] Deleted date should be null-able Fixes #240 --- bookwyrm/migrations/0056_auto_20201021_0150.py | 18 ++++++++++++++++++ bookwyrm/models/status.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/migrations/0056_auto_20201021_0150.py diff --git a/bookwyrm/migrations/0056_auto_20201021_0150.py b/bookwyrm/migrations/0056_auto_20201021_0150.py new file mode 100644 index 00000000..1408efb9 --- /dev/null +++ b/bookwyrm/migrations/0056_auto_20201021_0150.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-10-21 01:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0055_merge_20201017_0011'), + ] + + operations = [ + migrations.AlterField( + model_name='status', + name='deleted_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 0a70eb77..61e9c500 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -23,7 +23,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): # the created date can't be this, because of receiving federated posts published_date = models.DateTimeField(default=timezone.now) deleted = models.BooleanField(default=False) - deleted_date = models.DateTimeField() + deleted_date = models.DateTimeField(blank=True, null=True) favorites = models.ManyToManyField( 'User', symmetrical=False, From 6243cf0e4a763aacb5586338fbf6ef1127aa5349 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 26 Oct 2020 14:33:02 -0700 Subject: [PATCH 041/416] uses enum for post privacy database field --- bookwyrm/migrations/0057_auto_20201026_2131.py | 18 ++++++++++++++++++ bookwyrm/models/status.py | 13 ++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/migrations/0057_auto_20201026_2131.py diff --git a/bookwyrm/migrations/0057_auto_20201026_2131.py b/bookwyrm/migrations/0057_auto_20201026_2131.py new file mode 100644 index 00000000..cc414e98 --- /dev/null +++ b/bookwyrm/migrations/0057_auto_20201026_2131.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-10-26 21:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0056_auto_20201021_0150'), + ] + + operations = [ + migrations.AlterField( + model_name='status', + name='privacy', + field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + ), + ] diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 61e9c500..dfc39194 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -10,6 +10,13 @@ from .base_model import ActivitypubMixin, OrderedCollectionPageMixin from .base_model import ActivityMapping, BookWyrmModel +PrivacyLevels = models.TextChoices('Privacy', [ + 'public', + 'unlisted', + 'followers', + 'direct' +]) + class Status(OrderedCollectionPageMixin, BookWyrmModel): ''' any post, like a reply to a review, etc ''' user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -18,7 +25,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): mention_books = models.ManyToManyField( 'Edition', related_name='mention_book') local = models.BooleanField(default=True) - privacy = models.CharField(max_length=255, default='public') + privacy = models.CharField( + max_length=255, + default='public', + choices=PrivacyLevels.choices + ) sensitive = models.BooleanField(default=False) # the created date can't be this, because of receiving federated posts published_date = models.DateTimeField(default=timezone.now) From 2afa111b705eb319e3822e70d9c2f3ce0776d289 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 26 Oct 2020 15:00:15 -0700 Subject: [PATCH 042/416] Create statuses from django form --- bookwyrm/outgoing.py | 59 +++++++++++----------------------------- bookwyrm/status.py | 1 + bookwyrm/view_actions.py | 48 ++++++-------------------------- 3 files changed, 26 insertions(+), 82 deletions(-) diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index f496a211..118f90fe 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -9,9 +9,8 @@ import requests from bookwyrm import activitypub from bookwyrm import models from bookwyrm.broadcast import broadcast -from bookwyrm.status import create_review, create_status -from bookwyrm.status import create_quotation, create_comment -from bookwyrm.status import create_tag, create_notification, create_rating +from bookwyrm.status import create_status +from bookwyrm.status import create_tag, create_notification from bookwyrm.status import create_generated_note from bookwyrm.status import delete_status from bookwyrm.remote_user import get_or_create_remote_user @@ -178,16 +177,18 @@ def handle_import_books(user, items): broadcast(user, activity) if item.rating or item.review: - review_title = "Review of {!r} on Goodreads".format( - item.book.title, - ) if item.review else "" - handle_review( - user, - item.book, - review_title, - item.review, - item.rating, - ) + pass + #review_title = "Review of {!r} on Goodreads".format( + # item.book.title, + #) if item.review else "" + # TODO + #handle_review( + # user, + # item.book, + # review_title, + # item.review, + # item.rating, + #) for read in item.reads: read.book = item.book read.user = user @@ -209,37 +210,9 @@ def handle_delete_status(user, status): broadcast(user, status.to_activity()) -def handle_rate(user, book, rating): - ''' a review that's just a rating ''' - builder = create_rating - handle_status(user, book, builder, rating) - - -def handle_review(user, book, name, content, rating): - ''' post a review ''' - # validated and saves the review in the database so it has an id - builder = create_review - handle_status(user, book, builder, name, content, rating) - - -def handle_quotation(user, book, content, quote): - ''' post a review ''' - # validated and saves the review in the database so it has an id - builder = create_quotation - handle_status(user, book, builder, content, quote) - - -def handle_comment(user, book, content): - ''' post a comment ''' - # validated and saves the review in the database so it has an id - builder = create_comment - handle_status(user, book, builder, content) - - -def handle_status(user, book_id, builder, *args): +def handle_status(user, form): ''' generic handler for statuses ''' - book = models.Edition.objects.get(id=book_id) - status = builder(user, book, *args) + status = form.save() broadcast(user, status.to_create_activity(user), software='bookwyrm') diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 25619839..57c800e5 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -13,6 +13,7 @@ def delete_status(status): status.deleted_date = datetime.now() status.save() + def create_rating(user, book, rating): ''' a review that's just a rating ''' if not rating or rating < 1 or rating > 5: diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 54ed353a..e8a3b1fc 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -296,67 +296,37 @@ def shelve(request): def rate(request): ''' just a star rating for a book ''' form = forms.RatingForm(request.POST) - book_id = request.POST.get('book') - # TODO: better failure behavior - if not form.is_valid(): - return redirect('/book/%s' % book_id) - - rating = form.cleaned_data.get('rating') - # throws a value error if the book is not found - - outgoing.handle_rate(request.user, book_id, rating) - return redirect('/book/%s' % book_id) + return handle_status(request, form) @login_required def review(request): ''' create a book review ''' form = forms.ReviewForm(request.POST) - book_id = request.POST.get('book') - if not form.is_valid(): - return redirect('/book/%s' % book_id) - - # TODO: validation, htmlification - name = form.cleaned_data.get('name') - content = form.cleaned_data.get('content') - rating = form.data.get('rating', None) - try: - rating = int(rating) - except ValueError: - rating = None - - outgoing.handle_review(request.user, book_id, name, content, rating) - return redirect('/book/%s' % book_id) + return handle_status(request, form) @login_required def quotate(request): ''' create a book quotation ''' form = forms.QuotationForm(request.POST) - book_id = request.POST.get('book') - if not form.is_valid(): - return redirect('/book/%s' % book_id) - - quote = form.cleaned_data.get('quote') - content = form.cleaned_data.get('content') - - outgoing.handle_quotation(request.user, book_id, content, quote) - return redirect('/book/%s' % book_id) + return handle_status(request, form) @login_required def comment(request): ''' create a book comment ''' form = forms.CommentForm(request.POST) + return handle_status(request, form) + + +def handle_status(request, form): + ''' all the review/comment/quote etc functions are the same ''' book_id = request.POST.get('book') - # TODO: better failure behavior if not form.is_valid(): return redirect('/book/%s' % book_id) - # TODO: validation, htmlification - content = form.data.get('content') - - outgoing.handle_comment(request.user, book_id, content) + outgoing.handle_status(request.user, form) return redirect('/book/%s' % book_id) From 53891443185aa6f3008adf98df2c243c89122ed7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 26 Oct 2020 15:09:51 -0700 Subject: [PATCH 043/416] Fixes login validation form --- bookwyrm/templates/login.html | 3 --- bookwyrm/view_actions.py | 9 +-------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/bookwyrm/templates/login.html b/bookwyrm/templates/login.html index cbfd2b66..4e50c444 100644 --- a/bookwyrm/templates/login.html +++ b/bookwyrm/templates/login.html @@ -15,9 +15,6 @@
{{ login_form.username }}
- {% for error in login_form.username.errors %} -

{{ error | escape }}

- {% endfor %}
diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 54ed353a..3d01e9ff 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -24,14 +24,6 @@ def user_login(request): return redirect('/login') login_form = forms.LoginForm(request.POST) - register_form = forms.RegisterForm() - if not login_form.is_valid(): - data = { - 'site_settings': models.SiteSettings.get(), - 'login_form': login_form, - 'register_form': register_form - } - return TemplateResponse(request, 'login.html', data) username = login_form.data['username'] username = '%s@%s' % (username, DOMAIN) @@ -42,6 +34,7 @@ def user_login(request): return redirect(request.GET.get('next', '/')) login_form.non_field_errors = 'Username or password are incorrect' + register_form = forms.RegisterForm() data = { 'site_settings': models.SiteSettings.get(), 'login_form': login_form, From 39b9fe8f4a671bad786e757ea85fd5757184bdc2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 26 Oct 2020 15:10:32 -0700 Subject: [PATCH 044/416] Fixes serializing reviews with no rating --- bookwyrm/models/status.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index dfc39194..b1283c9d 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -192,9 +192,14 @@ class Review(Status): @property def ap_pure_name(self): ''' clarify review names for mastodon serialization ''' - return 'Review of "%s" (%d stars): %s' % ( + if self.rating: + return 'Review of "%s" (%d stars): %s' % ( + self.book.title, + self.rating, + self.name + ) + return 'Review of "%s": %s' % ( self.book.title, - self.rating, self.name ) From b7061c0f4d13576729a568d647be62e22ab40d4b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Oct 2020 11:32:15 -0700 Subject: [PATCH 045/416] Fixes create status forms --- bookwyrm/forms.py | 26 +++------------- bookwyrm/outgoing.py | 31 +++++++------------ .../templates/snippets/create_status.html | 8 +++++ bookwyrm/templates/snippets/interaction.html | 4 ++- bookwyrm/view_actions.py | 26 ++++++---------- 5 files changed, 38 insertions(+), 57 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 7b18a2ff..0c3b19ba 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -52,47 +52,31 @@ class RegisterForm(CustomForm): class RatingForm(CustomForm): class Meta: model = models.Review - fields = ['rating'] + fields = ['user', 'book', 'content', 'rating', 'privacy'] class ReviewForm(CustomForm): class Meta: model = models.Review - fields = ['name', 'content'] - help_texts = {f: None for f in fields} - labels = { - 'name': 'Title', - 'content': 'Review', - } + fields = ['user', 'book', 'name', 'content', 'rating', 'privacy'] class CommentForm(CustomForm): class Meta: model = models.Comment - fields = ['content'] - help_texts = {f: None for f in fields} - labels = { - 'content': 'Comment', - } + fields = ['user', 'book', 'content', 'privacy'] class QuotationForm(CustomForm): class Meta: model = models.Quotation - fields = ['quote', 'content'] - help_texts = {f: None for f in fields} - labels = { - 'quote': 'Quote', - 'content': 'Comment', - } + fields = ['user', 'book', 'quote', 'content', 'privacy'] class ReplyForm(CustomForm): class Meta: model = models.Status - fields = ['content'] - help_texts = {f: None for f in fields} - labels = {'content': 'Comment'} + fields = ['user', 'content', 'reply_parent', 'privacy'] class EditUserForm(CustomForm): diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 118f90fe..bcdf7170 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -9,7 +9,6 @@ import requests from bookwyrm import activitypub from bookwyrm import models from bookwyrm.broadcast import broadcast -from bookwyrm.status import create_status from bookwyrm.status import create_tag, create_notification from bookwyrm.status import create_generated_note from bookwyrm.status import delete_status @@ -214,12 +213,21 @@ def handle_status(user, form): ''' generic handler for statuses ''' status = form.save() + # notify reply parent or (TODO) tagged users + if status.reply_parent and status.reply_parent.user.local: + create_notification( + status.reply_parent.user, + 'REPLY', + related_user=user, + related_status=status + ) + broadcast(user, status.to_create_activity(user), software='bookwyrm') # re-format the activity for non-bookwyrm servers - remote_activity = status.to_create_activity(user, pure=True) - - broadcast(user, remote_activity, software='other') + if hasattr(status, 'pure_activity_serializer'): + remote_activity = status.to_create_activity(user, pure=True) + broadcast(user, remote_activity, software='other') def handle_tag(user, book, name): @@ -238,21 +246,6 @@ def handle_untag(user, book, name): broadcast(user, tag_activity) -def handle_reply(user, review, content): - ''' respond to a review or status ''' - # validated and saves the comment in the database so it has an id - reply = create_status(user, content, reply_parent=review) - if reply.reply_parent: - create_notification( - reply.reply_parent.user, - 'REPLY', - related_user=user, - related_status=reply, - ) - - broadcast(user, reply.to_create_activity(user)) - - def handle_favorite(user, status): ''' a user likes a status ''' try: diff --git a/bookwyrm/templates/snippets/create_status.html b/bookwyrm/templates/snippets/create_status.html index 88558df9..28eded97 100644 --- a/bookwyrm/templates/snippets/create_status.html +++ b/bookwyrm/templates/snippets/create_status.html @@ -22,6 +22,8 @@ - +
+
+ +
+ +
diff --git a/bookwyrm/templates/snippets/interaction.html b/bookwyrm/templates/snippets/interaction.html index 27bbbf8b..a48d8a7c 100644 --- a/bookwyrm/templates/snippets/interaction.html +++ b/bookwyrm/templates/snippets/interaction.html @@ -6,7 +6,7 @@ {% csrf_token %} - + From bdbf449dc07f7062fc2bd15f06c70771f967363d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 28 Oct 2020 13:17:02 -0700 Subject: [PATCH 054/416] Usbale navbar links --- bookwyrm/templates/layout.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index f5f72e65..55380652 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -47,11 +47,11 @@ + {% for readthrough in readthroughs %} +
+ {{ readthrough.start_date }} + {{ readthrough.finish_date }} + {{ readthrough.pages_read }} +
+ {% endfor %} + {% if request.user.is_authenticated %}
{% include 'snippets/create_status.html' with book=book hide_cover=True %} diff --git a/bookwyrm/tests/connectors/test_fedireads_connector.py b/bookwyrm/tests/connectors/test_fedireads_connector.py index 6645a936..5f475fdb 100644 --- a/bookwyrm/tests/connectors/test_fedireads_connector.py +++ b/bookwyrm/tests/connectors/test_fedireads_connector.py @@ -34,16 +34,6 @@ class BookWyrmConnector(TestCase): self.assertEqual(self.connector.is_work_data(self.edition_data), False) - def test_get_edition_from_work_data(self): - edition = self.connector.get_edition_from_work_data(self.work_data) - self.assertEqual(edition['url'], 'https://example.com/book/122') - - - def test_get_work_from_edition_data(self): - work = self.connector.get_work_from_edition_date(self.edition_data) - self.assertEqual(work['url'], 'https://example.com/book/121') - - def test_format_search_result(self): datafile = pathlib.Path(__file__).parent.joinpath('../data/fr_search.json') search_data = json.loads(datafile.read_bytes()) diff --git a/bookwyrm/tests/models/test_book_model.py b/bookwyrm/tests/models/test_book_model.py index 2cd980f8..78567a92 100644 --- a/bookwyrm/tests/models/test_book_model.py +++ b/bookwyrm/tests/models/test_book_model.py @@ -2,6 +2,7 @@ from django.test import TestCase from bookwyrm import models, settings +from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10 class Book(TestCase): @@ -48,6 +49,16 @@ class Book(TestCase): self.assertEqual(self.work.default_edition, self.second_edition) + def test_isbn_10_to_13(self): + isbn_10 = '178816167X' + isbn_13 = isbn_10_to_13(isbn_10) + self.assertEqual(isbn_13, '9781788161671') + + def test_isbn_13_to_10(self): + isbn_13 = '9781788161671' + isbn_10 = isbn_13_to_10(isbn_13) + self.assertEqual(isbn_10, '178816167X') + class Shelf(TestCase): def setUp(self): diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 2422cd27..9bf02959 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -112,26 +112,34 @@ def home_tab(request, tab): def get_activity_feed(user, filter_level, model=models.Status): ''' get a filtered queryset of statuses ''' # status updates for your follow network - following = models.User.objects.filter( - Q(followers=user) | Q(id=user.id) - ) + if user.is_anonymous: + user = None + if user: + following = models.User.objects.filter( + Q(followers=user) | Q(id=user.id) + ) + else: + following = [] activities = model if hasattr(model, 'objects'): - activities = model.objects.filter(deleted=False) + activities = model.objects - activities = activities.order_by( - '-created_date' + activities = activities.filter( + deleted=False + ).order_by( + '-published_date' ) + if hasattr(activities, 'select_subclasses'): activities = activities.select_subclasses() - # TODO: privacy relationshup between request.user and user if filter_level in ['friends', 'home']: # people you follow and direct mentions activities = activities.filter( - Q(user__in=following, privacy__in=['public', 'unlisted', 'followers']) | \ - Q(mention_users=user) | Q(user=user) + Q(user__in=following, privacy__in=[ + 'public', 'unlisted', 'followers' + ]) | Q(mention_users=user) | Q(user=user) ) elif filter_level == 'self': activities = activities.filter(user=user, privacy='public') @@ -470,14 +478,21 @@ def book_page(request, book_id): reviews = models.Review.objects.filter( book__in=work.edition_set.all(), - ).order_by('-published_date') + ) + reviews = get_activity_feed(request.user, 'federated', model=reviews) user_tags = [] + readthroughs = [] if request.user.is_authenticated: user_tags = models.Tag.objects.filter( book=book, user=request.user ).values_list('identifier', flat=True) + readthroughs = models.ReadThrough.objects.filter( + user=request.user + ).order_by('start_date') + + rating = reviews.aggregate(Avg('rating')) tags = models.Tag.objects.filter( book=book @@ -492,6 +507,7 @@ def book_page(request, book_id): 'rating': rating['rating__avg'], 'tags': tags, 'user_tags': user_tags, + 'readthroughs': readthroughs, 'review_form': forms.ReviewForm(), 'quotation_form': forms.QuotationForm(), 'comment_form': forms.CommentForm(), From a46d7f5dc7241d06bf18ac79876ebd94c70eac7d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 29 Oct 2020 14:29:31 -0700 Subject: [PATCH 058/416] Change how goodread import writes reviews - adds published date - broadcasts review imports - completes review and shelve actions as it goes - some small connector fixes fixes #247 --- bookwyrm/connectors/abstract_connector.py | 23 ++++--- bookwyrm/goodreads_import.py | 9 +-- bookwyrm/models/import_job.py | 11 +++- bookwyrm/outgoing.py | 78 +++++++++++------------ bookwyrm/views.py | 3 +- 5 files changed, 67 insertions(+), 57 deletions(-) diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 25db648c..72dda4e9 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -2,14 +2,16 @@ from abc import ABC, abstractmethod from dateutil import parser import pytz +from urllib3.exceptions import ProtocolError import requests +from requests import HTTPError from django.db import transaction from bookwyrm import models -class ConnectorException(Exception): +class ConnectorException(HTTPError): ''' when the connector can't do what was asked ''' @@ -155,9 +157,11 @@ class AbstractConnector(ABC): ''' for creating a new book or syncing with data ''' book = update_from_mappings(book, data, self.book_mappings) + author_text = [] for author in self.get_authors_from_data(data): book.authors.add(author) - book.author_text = ', '.join(a.display_name for a in book.authors.all()) + author_text += author.display_name + book.author_text = ', '.join(author_text) book.save() if not update_cover: @@ -287,12 +291,15 @@ def get_date(date_string): def get_data(url): ''' wrapper for request.get ''' - resp = requests.get( - url, - headers={ - 'Accept': 'application/json; charset=utf-8', - }, - ) + try: + resp = requests.get( + url, + headers={ + 'Accept': 'application/json; charset=utf-8', + }, + ) + except ProtocolError: + raise ConnectorException() if not resp.ok: resp.raise_for_status() data = resp.json() diff --git a/bookwyrm/goodreads_import.py b/bookwyrm/goodreads_import.py index 7b64baa3..73057e43 100644 --- a/bookwyrm/goodreads_import.py +++ b/bookwyrm/goodreads_import.py @@ -42,13 +42,10 @@ def import_data(job_id): if item.book: item.save() results.append(item) + # shelves book and handles reviews + outgoing.handle_imported_book(job.user, item) else: - item.fail_reason = "Could not match book on OpenLibrary" + item.fail_reason = "Could not find a match for book" item.save() - - status = outgoing.handle_import_books(job.user, results) - if status: - job.import_status = status - job.save() finally: create_notification(job.user, 'IMPORT', related_import=job) diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index 085cfea6..bd63ea79 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -40,8 +40,7 @@ class ImportJob(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) created_date = models.DateTimeField(default=timezone.now) task_id = models.CharField(max_length=100, null=True) - import_status = models.ForeignKey( - 'Status', null=True, on_delete=models.PROTECT) + class ImportItem(models.Model): ''' a single line of a csv being imported ''' @@ -71,6 +70,8 @@ class ImportItem(models.Model): return books_manager.get_or_create_book(search_result.key) except ConnectorException: pass + return None + def get_book_from_title_author(self): ''' search by title and author ''' @@ -84,6 +85,8 @@ class ImportItem(models.Model): return books_manager.get_or_create_book(search_result.key) except ConnectorException: pass + return None + @property def isbn(self): @@ -95,6 +98,7 @@ class ImportItem(models.Model): ''' the goodreads shelf field ''' if self.data['Exclusive Shelf']: return GOODREADS_SHELVES.get(self.data['Exclusive Shelf']) + return None @property def review(self): @@ -111,12 +115,14 @@ class ImportItem(models.Model): ''' when the book was added to this dataset ''' if self.data['Date Added']: return dateutil.parser.parse(self.data['Date Added']) + return None @property def date_read(self): ''' the date a book was completed ''' if self.data['Date Read']: return dateutil.parser.parse(self.data['Date Read']) + return None @property def reads(self): @@ -126,6 +132,7 @@ class ImportItem(models.Model): return [ReadThrough(start_date=self.date_added)] if self.date_read: return [ReadThrough( + start_date=self.date_added, finish_date=self.date_read, )] return [] diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 4681a670..eca3a2c8 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -155,51 +155,49 @@ def handle_unshelve(user, book, shelf): broadcast(user, activity) -def handle_import_books(user, items): +def handle_imported_book(user, item): ''' process a goodreads csv and then post about it ''' - new_books = [] - for item in items: - if item.shelf: - desired_shelf = models.Shelf.objects.get( - identifier=item.shelf, - user=user - ) - if isinstance(item.book, models.Work): - item.book = item.book.default_edition - if not item.book: - continue - shelf_book, created = models.ShelfBook.objects.get_or_create( - book=item.book, shelf=desired_shelf, added_by=user) - if created: - new_books.append(item.book) - activity = shelf_book.to_add_activity(user) - broadcast(user, activity) + if isinstance(item.book, models.Work): + item.book = item.book.default_edition + if not item.book: + return - if item.rating or item.review: - review_title = 'Review of {!r} on Goodreads'.format( - item.book.title, - ) if item.review else '' + if item.shelf: + desired_shelf = models.Shelf.objects.get( + identifier=item.shelf, + user=user + ) + # shelve the book if it hasn't been shelved already + shelf_book, created = models.ShelfBook.objects.get_or_create( + book=item.book, shelf=desired_shelf, added_by=user) + if created: + broadcast(user, shelf_book.to_add_activity(user)) - models.Review.objects.create( - user=user, - book=item.book, - name=review_title, - content=item.review, - rating=item.rating, - ) - for read in item.reads: - read.book = item.book - read.user = user - read.save() + # only add new read-throughs if the item isn't already shelved + for read in item.reads: + read.book = item.book + read.user = user + read.save() - if new_books: - message = 'imported {} books'.format(len(new_books)) - status = create_generated_note(user, message, mention_books=new_books) - status.save() + if item.rating or item.review: + review_title = 'Review of {!r} on Goodreads'.format( + item.book.title, + ) if item.review else '' - broadcast(user, status.to_create_activity(user)) - return status - return None + # we don't know the publication date of the review, + # but "now" is a bad guess + published_date_guess = item.date_read or item.date_added + review = models.Review.objects.create( + user=user, + book=item.book, + name=review_title, + content=item.review, + rating=item.rating, + published_date=published_date_guess, + ) + # we don't need to send out pure activities because non-bookwyrm + # instances don't need this data + broadcast(user, review.to_create_activity(user)) def handle_delete_status(user, status): diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 9bf02959..b1bb25f4 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -489,7 +489,8 @@ def book_page(request, book_id): ).values_list('identifier', flat=True) readthroughs = models.ReadThrough.objects.filter( - user=request.user + user=request.user, + book=book, ).order_by('start_date') From 7ce0890a41d98a15fc79ac16cbbef0aab0eed22e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 29 Oct 2020 15:29:23 -0700 Subject: [PATCH 059/416] Stop assuming every book is Hamlet --- bookwyrm/books_manager.py | 12 ++++++------ bookwyrm/connectors/abstract_connector.py | 19 ++++++++++--------- bookwyrm/connectors/openlibrary.py | 8 ++++---- bookwyrm/connectors/self_connector.py | 13 +++++++------ bookwyrm/models/import_job.py | 8 ++++++-- bookwyrm/templates/search_results.html | 1 + bookwyrm/tests/models/test_import_model.py | 8 +++----- 7 files changed, 37 insertions(+), 32 deletions(-) diff --git a/bookwyrm/books_manager.py b/bookwyrm/books_manager.py index bfc543de..37a31766 100644 --- a/bookwyrm/books_manager.py +++ b/bookwyrm/books_manager.py @@ -64,14 +64,14 @@ def load_more_data(book_id): connector.expand_book_data(book) -def search(query): +def search(query, min_confidence=0.1): ''' find books based on arbitary keywords ''' results = [] dedup_slug = lambda r: '%s/%s/%s' % (r.title, r.author, r.year) result_index = set() for connector in get_connectors(): try: - result_set = connector.search(query) + result_set = connector.search(query, min_confidence=min_confidence) except HTTPError: continue @@ -87,16 +87,16 @@ def search(query): return results -def local_search(query): +def local_search(query, min_confidence=0.1): ''' only look at local search results ''' connector = load_connector(models.Connector.objects.get(local=True)) - return connector.search(query) + return connector.search(query, min_confidence=min_confidence) -def first_search_result(query): +def first_search_result(query, min_confidence=0.1): ''' search until you find a result that fits ''' for connector in get_connectors(): - result = connector.search(query) + result = connector.search(query, min_confidence=min_confidence) if result: return result[0] return None diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 72dda4e9..a34eb301 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -1,8 +1,8 @@ ''' functionality outline for a book data connector ''' from abc import ABC, abstractmethod +from dataclasses import dataclass from dateutil import parser import pytz -from urllib3.exceptions import ProtocolError import requests from requests import HTTPError @@ -52,7 +52,7 @@ class AbstractConnector(ABC): return True - def search(self, query): + def search(self, query, min_confidence=None): ''' free text search ''' resp = requests.get( '%s%s' % (self.search_url, query), @@ -160,7 +160,7 @@ class AbstractConnector(ABC): author_text = [] for author in self.get_authors_from_data(data): book.authors.add(author) - author_text += author.display_name + author_text.append(author.display_name) book.author_text = ', '.join(author_text) book.save() @@ -298,7 +298,7 @@ def get_data(url): 'Accept': 'application/json; charset=utf-8', }, ) - except ProtocolError: + except ConnectionError: raise ConnectorException() if not resp.ok: resp.raise_for_status() @@ -306,13 +306,14 @@ def get_data(url): return data +@dataclass class SearchResult: ''' standardized search result object ''' - def __init__(self, title, key, author, year): - self.title = title - self.key = key - self.author = author - self.year = year + title: str + key: str + author: str + year: str + confidence: int = 1 def __repr__(self): return "".format( diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index d70ab3e2..0ae3ce35 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -129,10 +129,10 @@ class Connector(AbstractConnector): key = self.books_url + search_result['key'] author = search_result.get('author_name') or ['Unknown'] return SearchResult( - search_result.get('title'), - key, - ', '.join(author), - search_result.get('first_publish_year'), + title=search_result.get('title'), + key=key, + author=', '.join(author), + year=search_result.get('first_publish_year'), ) diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index 2711bb1a..0e77ecf6 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -7,7 +7,7 @@ from .abstract_connector import AbstractConnector, SearchResult class Connector(AbstractConnector): ''' instantiate a connector ''' - def search(self, query): + def search(self, query, min_confidence=0.1): ''' right now you can't search bookwyrm sorry, but when that gets implemented it will totally rule ''' vector = SearchVector('title', weight='A') +\ @@ -28,7 +28,7 @@ class Connector(AbstractConnector): ).annotate( rank=SearchRank(vector, query) ).filter( - rank__gt=0 + rank__gt=min_confidence ).order_by('-rank') results = results.filter(default=True) or results @@ -42,11 +42,12 @@ class Connector(AbstractConnector): def format_search_result(self, search_result): return SearchResult( - search_result.title, - search_result.local_id, - search_result.author_text, - search_result.published_date.year if \ + title=search_result.title, + key=search_result.local_id, + author=search_result.author_text, + year=search_result.published_date.year if \ search_result.published_date else None, + confidence=search_result.rank, ) diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index bd63ea79..240e0694 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -63,7 +63,9 @@ class ImportItem(models.Model): def get_book_from_isbn(self): ''' search by isbn ''' - search_result = books_manager.first_search_result(self.isbn) + search_result = books_manager.first_search_result( + self.isbn, min_confidence=0.5 + ) if search_result: try: # don't crash the import when the connector fails @@ -79,7 +81,9 @@ class ImportItem(models.Model): self.data['Title'], self.data['Author'] ) - search_result = books_manager.first_search_result(search_term) + search_result = books_manager.first_search_result( + search_term, min_confidence=0.5 + ) if search_result: try: return books_manager.get_or_create_book(search_result.key) diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html index bd5096fe..489386cd 100644 --- a/bookwyrm/templates/search_results.html +++ b/bookwyrm/templates/search_results.html @@ -14,6 +14,7 @@ {% for result in result_set.results %}
+ {{ result.confidence }}
{% csrf_token %} diff --git a/bookwyrm/tests/models/test_import_model.py b/bookwyrm/tests/models/test_import_model.py index 5e488199..1d5aaa72 100644 --- a/bookwyrm/tests/models/test_import_model.py +++ b/bookwyrm/tests/models/test_import_model.py @@ -24,7 +24,7 @@ class ImportJob(TestCase): 'Number of Pages': 416, 'Year Published': 2019, 'Original Publication Year': 2019, - 'Date Read': '2019/04/09', + 'Date Read': '2019/04/12', 'Date Added': '2019/04/09', 'Bookshelves': '', 'Bookshelves with positions': '', @@ -97,11 +97,9 @@ class ImportJob(TestCase): self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date) def test_read_reads(self): - expected = [models.ReadThrough( - finish_date=datetime.datetime(2019, 4, 9, 0, 0))] actual = models.ImportItem.objects.get(index=2) - self.assertEqual(actual.reads[0].start_date, expected[0].start_date) - self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date) + self.assertEqual(actual.reads[0].start_date, datetime.datetime(2019, 4, 9, 0, 0)) + self.assertEqual(actual.reads[0].finish_date, datetime.datetime(2019, 4, 12, 0, 0)) def test_unread_reads(self): expected = [] From cca98d90513ce342dc1a29e71c6bd01459969072 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 29 Oct 2020 16:08:23 -0700 Subject: [PATCH 060/416] removes confidence displaying in search results page --- bookwyrm/templates/search_results.html | 1 - 1 file changed, 1 deletion(-) diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html index 489386cd..bd5096fe 100644 --- a/bookwyrm/templates/search_results.html +++ b/bookwyrm/templates/search_results.html @@ -14,7 +14,6 @@ {% for result in result_set.results %}
- {{ result.confidence }} {% csrf_token %} From 7fb593af8cff15f6d06e92a9404dcd30f90f60b2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 29 Oct 2020 16:48:28 -0700 Subject: [PATCH 061/416] Remove status associated with import --- .../0058_remove_importjob_import_status.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 bookwyrm/migrations/0058_remove_importjob_import_status.py diff --git a/bookwyrm/migrations/0058_remove_importjob_import_status.py b/bookwyrm/migrations/0058_remove_importjob_import_status.py new file mode 100644 index 00000000..61f41546 --- /dev/null +++ b/bookwyrm/migrations/0058_remove_importjob_import_status.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.7 on 2020-10-29 23:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0057_auto_20201026_2131'), + ] + + operations = [ + migrations.RemoveField( + model_name='importjob', + name='import_status', + ), + ] From 5641c36539555938f0a47f0f82cddeb6d33c39c1 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 29 Oct 2020 22:38:01 -0700 Subject: [PATCH 062/416] UI for editable readthroughs --- bookwyrm/forms.py | 7 +++++ bookwyrm/templates/book.html | 58 +++++++++++++++++++++++++++++++++--- bookwyrm/views.py | 3 +- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 0c3b19ba..1edc5d5c 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -29,6 +29,7 @@ class CustomForm(ModelForm): visible.field.widget.attrs['rows'] = None visible.field.widget.attrs['class'] = css_classes[input_type] + class LoginForm(CustomForm): class Meta: model = models.User @@ -158,3 +159,9 @@ class CreateInviteForm(CustomForm): choices=[(i, "%d uses" % (i,)) for i in [1, 5, 10, 25, 50, 100]] + [(None, 'Unlimited')]) } + + +class ReadThroughForm(CustomForm): + class Meta: + model = models.ReadThrough + fields = ['user', 'book', 'start_date', 'finish_date'] diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index c5d529ed..8b169600 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -56,10 +56,60 @@
{% for readthrough in readthroughs %} -
- {{ readthrough.start_date }} - {{ readthrough.finish_date }} - {{ readthrough.pages_read }} +
+ + +
+ +
+ +
{% endfor %} diff --git a/bookwyrm/views.py b/bookwyrm/views.py index b1bb25f4..67586a22 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -493,7 +493,6 @@ def book_page(request, book_id): book=book, ).order_by('start_date') - rating = reviews.aggregate(Avg('rating')) tags = models.Tag.objects.filter( book=book @@ -508,10 +507,10 @@ def book_page(request, book_id): 'rating': rating['rating__avg'], 'tags': tags, 'user_tags': user_tags, - 'readthroughs': readthroughs, 'review_form': forms.ReviewForm(), 'quotation_form': forms.QuotationForm(), 'comment_form': forms.CommentForm(), + 'readthroughs': readthroughs, 'tag_form': forms.TagForm(), 'path': '/book/%s' % book_id, 'cover_form': forms.CoverForm(instance=book), From 45f39fab480c6ced9df2435442642d726e24aa42 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 29 Oct 2020 23:28:23 -0700 Subject: [PATCH 063/416] upping confidence for import search better to query OL than to get the wrong book locally --- bookwyrm/models/import_job.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index 240e0694..54b6c438 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -64,7 +64,7 @@ class ImportItem(models.Model): def get_book_from_isbn(self): ''' search by isbn ''' search_result = books_manager.first_search_result( - self.isbn, min_confidence=0.5 + self.isbn, min_confidence=0.992 ) if search_result: try: @@ -82,7 +82,7 @@ class ImportItem(models.Model): self.data['Author'] ) search_result = books_manager.first_search_result( - search_term, min_confidence=0.5 + search_term, min_confidence=0.992 ) if search_result: try: From e39bf026cb8cc4f4f485609c7fd7b19f72711afa Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 10:40:05 -0700 Subject: [PATCH 064/416] Handle edit and delete readthroughs --- bookwyrm/templates/book.html | 12 ++++----- bookwyrm/urls.py | 3 +++ bookwyrm/view_actions.py | 47 ++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index 8b169600..f05fd7ed 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -73,7 +73,7 @@
{% csrf_token %} - + +
+ {{ import_form.as_p }} +
+
+ +
+
+ +
+ +
+
+

Imports are limited in size, and only the first {{ limit }} items will be imported. diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index c3c93784..299ead10 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -491,12 +491,16 @@ def import_data(request): ''' 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') try: job = goodreads_import.create_job( request.user, TextIOWrapper( request.FILES['csv_file'], - encoding=request.encoding) + encoding=request.encoding), + include_reviews, + privacy, ) except (UnicodeDecodeError, ValueError): return HttpResponseBadRequest('Not a valid csv file') From 701be2610071873bb6bb962ee4138f3a2ba3a452 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 11:55:41 -0700 Subject: [PATCH 067/416] Fixes unit tests --- bookwyrm/tests/connectors/test_self_connector.py | 2 +- bookwyrm/tests/models/test_import_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/tests/connectors/test_self_connector.py b/bookwyrm/tests/connectors/test_self_connector.py index 47c0b454..3dd4ac12 100644 --- a/bookwyrm/tests/connectors/test_self_connector.py +++ b/bookwyrm/tests/connectors/test_self_connector.py @@ -48,7 +48,7 @@ class SelfConnector(TestCase): def test_format_search_result(self): - result = self.connector.format_search_result(self.edition) + result = self.connector.search('Edition of Example')[0] self.assertEqual(result.title, 'Edition of Example Work') self.assertEqual(result.key, self.edition.remote_id) self.assertEqual(result.author, 'Anonymous') diff --git a/bookwyrm/tests/models/test_import_model.py b/bookwyrm/tests/models/test_import_model.py index 1d5aaa72..5e9a9e30 100644 --- a/bookwyrm/tests/models/test_import_model.py +++ b/bookwyrm/tests/models/test_import_model.py @@ -84,7 +84,7 @@ class ImportJob(TestCase): def test_date_read(self): ''' converts to the local shelf typology ''' - expected = datetime.datetime(2019, 4, 9, 0, 0) + expected = datetime.datetime(2019, 4, 12, 0, 0) item = models.ImportItem.objects.get(index=2) self.assertEqual(item.date_read, expected) From 9780879ce64f2976e7525f6321eec1a33b496c2b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 12:07:22 -0700 Subject: [PATCH 068/416] Still shelve books in no-reviews import mode --- bookwyrm/goodreads_import.py | 6 +++--- bookwyrm/outgoing.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bookwyrm/goodreads_import.py b/bookwyrm/goodreads_import.py index 9e2bcd1d..fe5ac56e 100644 --- a/bookwyrm/goodreads_import.py +++ b/bookwyrm/goodreads_import.py @@ -47,9 +47,9 @@ def import_data(job_id): item.save() results.append(item) - if job.include_reviews: - # shelves book and handles reviews - outgoing.handle_imported_book(job.user, item, job.privacy) + # shelves book and handles reviews + outgoing.handle_imported_book( + job.user, item, job.include_reviews, job.privacy) else: item.fail_reason = "Could not find a match for book" item.save() diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 2ff8c9fa..908f3b5b 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -155,7 +155,7 @@ def handle_unshelve(user, book, shelf): broadcast(user, activity) -def handle_imported_book(user, item, privacy): +def handle_imported_book(user, item, include_reviews, privacy): ''' process a goodreads csv and then post about it ''' if isinstance(item.book, models.Work): item.book = item.book.default_edition @@ -179,7 +179,7 @@ def handle_imported_book(user, item, privacy): read.user = user read.save() - if item.rating or item.review: + if include_reviews and (item.rating or item.review): review_title = 'Review of {!r} on Goodreads'.format( item.book.title, ) if item.review else '' From 3fc1f46897e4ff2d0bc3fcd2cfa7005359cbaed6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 12:43:02 -0700 Subject: [PATCH 069/416] Handle dashes in isbns --- bookwyrm/models/book.py | 3 +++ bookwyrm/tests/models/test_book_model.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index e32846fe..c8b18f8e 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -186,6 +186,7 @@ class Edition(Book): def isbn_10_to_13(isbn_10): ''' convert an isbn 10 into an isbn 13 ''' + isbn_10 = isbn_10.replace('-', '') # drop the last character of the isbn 10 number (the original checkdigit) converted = isbn_10[:9] # add "978" to the front @@ -206,6 +207,8 @@ def isbn_13_to_10(isbn_13): if isbn_13[:3] != '978': return None + isbn_13 = isbn_13.replace('-', '') + # remove '978' and old checkdigit converted = isbn_13[3:-1] # calculate checkdigit diff --git a/bookwyrm/tests/models/test_book_model.py b/bookwyrm/tests/models/test_book_model.py index 78567a92..e8211a8f 100644 --- a/bookwyrm/tests/models/test_book_model.py +++ b/bookwyrm/tests/models/test_book_model.py @@ -54,11 +54,21 @@ class Book(TestCase): isbn_13 = isbn_10_to_13(isbn_10) self.assertEqual(isbn_13, '9781788161671') + isbn_10 = '1-788-16167-X' + isbn_13 = isbn_10_to_13(isbn_10) + self.assertEqual(isbn_13, '9781788161671') + + + def test_isbn_13_to_10(self): isbn_13 = '9781788161671' isbn_10 = isbn_13_to_10(isbn_13) self.assertEqual(isbn_10, '178816167X') + isbn_13 = '978-1788-16167-1' + isbn_10 = isbn_13_to_10(isbn_13) + self.assertEqual(isbn_10, '178816167X') + class Shelf(TestCase): def setUp(self): From 3ca50a7573759d359b5cb045c6dbc0ee93b7e694 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 12:57:31 -0700 Subject: [PATCH 070/416] safer isbn normalization --- bookwyrm/models/book.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index c8b18f8e..d6b48b57 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -1,4 +1,6 @@ ''' database schema for books and shelves ''' +import re + from django.db import models from django.utils import timezone from django.utils.http import http_date @@ -186,15 +188,18 @@ class Edition(Book): def isbn_10_to_13(isbn_10): ''' convert an isbn 10 into an isbn 13 ''' - isbn_10 = isbn_10.replace('-', '') + 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 # add a check digit to the end # multiply the odd digits by 1 and the even digits by 3 and sum them - checksum = sum(int(i) for i in converted[::2]) + \ + try: + 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 checkdigit = checksum % 10 if checkdigit != 0: @@ -207,13 +212,16 @@ def isbn_13_to_10(isbn_13): if isbn_13[:3] != '978': return None - isbn_13 = isbn_13.replace('-', '') + isbn_13 = re.sub(r'[^0-9X]', '', isbn_13) # remove '978' and old checkdigit converted = isbn_13[3:-1] # calculate checkdigit # multiple each digit by 10,9,8.. successively and sum them - checksum = sum(int(d) * (10 - idx) for (idx, d) in enumerate(converted)) + try: + checksum = sum(int(d) * (10 - idx) for (idx, d) in enumerate(converted)) + except ValueError: + return None checkdigit = checksum % 11 checkdigit = 11 - checkdigit if checkdigit == 10: From c9354a5ad134cc0f83936c517b56a49c3954c017 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 13:11:13 -0700 Subject: [PATCH 071/416] Make federated server blankable --- .../migrations/0060_auto_20201030_2010.py | 19 +++++++++++++++++++ bookwyrm/models/user.py | 1 + 2 files changed, 20 insertions(+) create mode 100644 bookwyrm/migrations/0060_auto_20201030_2010.py diff --git a/bookwyrm/migrations/0060_auto_20201030_2010.py b/bookwyrm/migrations/0060_auto_20201030_2010.py new file mode 100644 index 00000000..bcc28c5c --- /dev/null +++ b/bookwyrm/migrations/0060_auto_20201030_2010.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-10-30 20:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0059_auto_20201030_1755'), + ] + + operations = [ + 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'), + ), + ] diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 0cd8b978..d1e1f69e 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -24,6 +24,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): 'FederatedServer', on_delete=models.PROTECT, null=True, + blank=True, ) outbox = models.CharField(max_length=255, unique=True) summary = models.TextField(blank=True, null=True) From eece662ec1ebe153834b74f99ffc11ca5b0b7a8c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 14:01:43 -0700 Subject: [PATCH 072/416] tab through user shelves --- bookwyrm/templates/shelf.html | 15 ++++++++------ bookwyrm/templates/snippets/shelf.html | 2 +- bookwyrm/templates/user_shelves.html | 13 ++++++++++++- bookwyrm/views.py | 27 +++++++++++++------------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/bookwyrm/templates/shelf.html b/bookwyrm/templates/shelf.html index 2c49f002..96279543 100644 --- a/bookwyrm/templates/shelf.html +++ b/bookwyrm/templates/shelf.html @@ -1,19 +1,22 @@ {% extends 'layout.html' %} +{% load fr_display %} {% block content %} +{% include 'user_header.html' with user=user %}

- +
-

{{ shelf.name }}

{% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %}
diff --git a/bookwyrm/templates/snippets/shelf.html b/bookwyrm/templates/snippets/shelf.html index f8acf56c..adee004e 100644 --- a/bookwyrm/templates/snippets/shelf.html +++ b/bookwyrm/templates/snippets/shelf.html @@ -1,6 +1,6 @@ {% load humanize %} {% load fr_display %} -{% if shelf.books %} +{% if shelf.books.all|length > 0 %} diff --git a/bookwyrm/templates/user_shelves.html b/bookwyrm/templates/user_shelves.html index 239fd592..af4f9d23 100644 --- a/bookwyrm/templates/user_shelves.html +++ b/bookwyrm/templates/user_shelves.html @@ -3,9 +3,20 @@ {% block content %} {% include 'user_header.html' with user=user %} +
+
+ +

{{ shelf.name }}

+
+ {% for shelf in shelves %}
-

{{ shelf.name }}

{% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %}
{% endfor %} diff --git a/bookwyrm/views.py b/bookwyrm/views.py index b1bb25f4..8c1dea2c 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -299,7 +299,7 @@ def notifications_page(request): return TemplateResponse(request, 'notifications.html', data) @csrf_exempt -def user_page(request, username, subpage=None): +def user_page(request, username, subpage=None, shelf=None): ''' profile page for a user ''' try: user = get_user_from_username(username) @@ -323,18 +323,22 @@ def user_page(request, username, subpage=None): return TemplateResponse(request, 'following.html', data) if subpage == 'shelves': data['shelves'] = user.shelf_set.all() - return TemplateResponse(request, 'user_shelves.html', data) + if shelf: + data['shelf'] = user.shelf_set.get(identifier=shelf) + else: + data['shelf'] = user.shelf_set.first() + return TemplateResponse(request, 'shelf.html', data) data['shelf_count'] = user.shelf_set.count() shelves = [] - for shelf in user.shelf_set.all(): - if not shelf.books.count(): + for user_shelf in user.shelf_set.all(): + if not user_shelf.books.count(): continue shelves.append({ - 'name': shelf.name, - 'remote_id': shelf.remote_id, - 'books': shelf.books.all()[:3], - 'size': shelf.books.count(), + 'name': user_shelf.name, + 'remote_id': user_shelf.remote_id, + 'books': user_shelf.books.all()[:3], + 'size': user_shelf.books.count(), }) if len(shelves) > 2: break @@ -600,8 +604,5 @@ def shelf_page(request, username, shelf_identifier): if is_api_request(request): return JsonResponse(shelf.to_activity(**request.GET)) - data = { - 'shelf': shelf, - 'user': user, - } - return TemplateResponse(request, 'shelf.html', data) + return user_page( + request, username, subpage='shelves', shelf=shelf_identifier) From 6e97592518843c6aa6accbb88a1c41fa19d53f3f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 14:19:43 -0700 Subject: [PATCH 073/416] Trying to catch more http request errors --- bookwyrm/connectors/abstract_connector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index a34eb301..3172ea79 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -5,6 +5,7 @@ from dateutil import parser import pytz import requests from requests import HTTPError +from urllib3.exceptions import RequestError from django.db import transaction @@ -298,7 +299,7 @@ def get_data(url): 'Accept': 'application/json; charset=utf-8', }, ) - except ConnectionError: + except RequestError: raise ConnectorException() if not resp.ok: resp.raise_for_status() From 95455d95386c52206cac0e6ed32f1399f71b15c9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 14:43:39 -0700 Subject: [PATCH 074/416] Preserve linebreaks in text --- bookwyrm/static/css/format.css | 5 +++++ bookwyrm/templates/snippets/book_description.html | 6 +----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/format.css index c37131f4..b1bf20cd 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/format.css @@ -116,3 +116,8 @@ input.toggle-control:checked ~ .toggle-content { content: "\e903"; right: 0; } + +/* --- BLOCKQUOTE --- */ +blockquote { + white-space: pre-line; +} diff --git a/bookwyrm/templates/snippets/book_description.html b/bookwyrm/templates/snippets/book_description.html index f5fb3f43..12c9ccfb 100644 --- a/bookwyrm/templates/snippets/book_description.html +++ b/bookwyrm/templates/snippets/book_description.html @@ -1,6 +1,2 @@ -{% if book.description %} -
{{ book.description }}
-{% elif book.parent_work.description %} -
{{ book.parent_work.description }}
-{% endif %} +
{% if book.description %}{{ book.description }}{% elif book.parent_work.description %}{{ book.parent_work.description }}{% endif %}
From a17f54e45742e4f35d97b037767831c2b3ee9d27 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 15:22:20 -0700 Subject: [PATCH 075/416] Fixes federation bugs --- bookwyrm/incoming.py | 2 +- bookwyrm/migrations/0061_auto_20201030_2157.py | 17 +++++++++++++++++ bookwyrm/models/__init__.py | 4 ++-- bookwyrm/models/base_model.py | 3 +++ bookwyrm/models/book.py | 13 ++----------- bookwyrm/models/status.py | 13 ++++--------- bookwyrm/status.py | 2 +- 7 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 bookwyrm/migrations/0061_auto_20201030_2157.py diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index e137aafa..7507ee5b 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -204,7 +204,7 @@ def handle_follow_reject(activity): def handle_create(activity): ''' someone did something, good on them ''' if activity['object'].get('type') not in \ - ['Note', 'Comment', 'Quotation', 'Review']: + ['Note', 'Comment', 'Quotation', 'Review', 'GeneratedNote']: # if it's an article or unknown type, ignore it return diff --git a/bookwyrm/migrations/0061_auto_20201030_2157.py b/bookwyrm/migrations/0061_auto_20201030_2157.py new file mode 100644 index 00000000..750b3763 --- /dev/null +++ b/bookwyrm/migrations/0061_auto_20201030_2157.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.7 on 2020-10-30 21:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0060_auto_20201030_2010'), + ] + + operations = [ + migrations.RenameModel( + old_name='GeneratedStatus', + new_name='GeneratedNote', + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 47ae177b..18ddb804 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -6,7 +6,7 @@ from .book import Book, Work, Edition, Author from .connector import Connector from .relationship import UserFollows, UserFollowRequest, UserBlocks from .shelf import Shelf, ShelfBook -from .status import Status, GeneratedStatus, Review, Comment, Quotation +from .status import Status, GeneratedNote, Review, Comment, Quotation from .status import Favorite, Boost, Notification, ReadThrough from .tag import Tag from .user import User @@ -16,5 +16,5 @@ from .import_job import ImportJob, ImportItem from .site import SiteSettings, SiteInvite, PasswordReset cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) -activity_models = {c[0]: c[1].activity_serializer for c in cls_members \ +activity_models = {c[0]: c[1] for c in cls_members \ if hasattr(c[1], 'activity_serializer')} diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index d5f4a899..7d3cb344 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -1,4 +1,5 @@ ''' base model with default fields ''' +from datetime import datetime from base64 import b64encode from dataclasses import dataclass from typing import Callable @@ -69,6 +70,8 @@ class ActivitypubMixin: value = getattr(self, mapping.model_key) if hasattr(value, 'remote_id'): value = value.remote_id + if isinstance(value, datetime): + value = value.isoformat() fields[mapping.activity_key] = mapping.activity_formatter(value) if pure: diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index d6b48b57..03b2c1f8 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -3,7 +3,6 @@ import re from django.db import models from django.utils import timezone -from django.utils.http import http_date from model_utils.managers import InheritanceManager from bookwyrm import activitypub @@ -63,16 +62,8 @@ class Book(ActivitypubMixin, BookWyrmModel): ActivityMapping('id', 'remote_id'), ActivityMapping('authors', 'ap_authors'), - ActivityMapping( - 'first_published_date', - 'first_published_date', - activity_formatter=lambda d: http_date(d.timestamp()) if d else None - ), - ActivityMapping( - 'published_date', - 'published_date', - activity_formatter=lambda d: http_date(d.timestamp()) if d else None - ), + ActivityMapping('first_published_date', 'first_published_date'), + ActivityMapping('published_date', 'published_date'), ActivityMapping('title', 'title'), ActivityMapping('sort_title', 'sort_title'), diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 533868e8..8798589f 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -1,6 +1,5 @@ ''' models for storing different kinds of Activities ''' from django.utils import timezone -from django.utils.http import http_date from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from model_utils.managers import InheritanceManager @@ -62,11 +61,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ActivityMapping('id', 'remote_id'), ActivityMapping('url', 'remote_id'), ActivityMapping('inReplyTo', 'reply_parent'), - ActivityMapping( - 'published', - 'published_date', - activity_formatter=lambda d: http_date(d.timestamp()) - ), + ActivityMapping('published', 'published_date'), ActivityMapping('attributedTo', 'user'), ActivityMapping('to', 'ap_to'), ActivityMapping('cc', 'ap_cc'), @@ -116,13 +111,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): return activitypub.Tombstone( id=self.remote_id, url=self.remote_id, - deleted=http_date(self.deleted_date.timestamp()), - published=http_date(self.deleted_date.timestamp()), + deleted=self.deleted_date.isoformat(), + published=self.deleted_date.isoformat() ).serialize() return ActivitypubMixin.to_activity(self, **kwargs) -class GeneratedStatus(Status): +class GeneratedNote(Status): ''' these are app-generated messages about user activity ''' @property def ap_pure_content(self): diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 258734b3..b373bec5 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -21,7 +21,7 @@ def create_generated_note(user, content, mention_books=None): parser.feed(content) content = parser.get_output() - status = models.GeneratedStatus.objects.create( + status = models.GeneratedNote.objects.create( user=user, content=content, ) From 203e526a8385811f6684fc93150cd9b91757b07d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 17:04:10 -0700 Subject: [PATCH 076/416] Fixes loading remote books - saves remote_id correctly - loads remote books for incoming statuses --- bookwyrm/activitypub/__init__.py | 2 +- bookwyrm/activitypub/note.py | 8 ++++++++ bookwyrm/connectors/abstract_connector.py | 14 +++++++++++--- bookwyrm/connectors/bookwyrm_connector.py | 6 +++++- bookwyrm/connectors/openlibrary.py | 8 ++++++++ bookwyrm/connectors/self_connector.py | 3 +++ bookwyrm/incoming.py | 9 +++++++++ bookwyrm/models/status.py | 12 ++++++++++++ 8 files changed, 57 insertions(+), 5 deletions(-) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 446455fa..c10d1ca1 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -4,7 +4,7 @@ import sys from .base_activity import ActivityEncoder, Image, PublicKey, Signature from .note import Note, GeneratedNote, Article, Comment, Review, Quotation -from .note import Tombstone +from .note import Tombstone, Link from .interaction import Boost, Like from .ordered_collection import OrderedCollection, OrderedCollectionPage from .person import Person diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 4e36b01f..d187acc6 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -36,9 +36,17 @@ class Article(Note): type: str = 'Article' +@dataclass +class Link(): + ''' for tagging a book in a status ''' + href: str + name: str + type: str = 'Link' + @dataclass(init=False) class GeneratedNote(Note): ''' just a re-typed note ''' + tag: List[Link] type: str = 'GeneratedNote' diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 3172ea79..c6e2f19e 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -122,11 +122,11 @@ class AbstractConnector(ABC): # atomic so that we don't save a work with no edition for vice versa with transaction.atomic(): if not work: - work_key = work_data.get('url') + work_key = self.get_remote_id_from_data(work_data) work = self.create_book(work_key, work_data, models.Work) if not edition: - ed_key = edition_data.get('url') + ed_key = self.get_remote_id_from_data(edition_data) edition = self.create_book(ed_key, edition_data, models.Edition) edition.default = True edition.parent_work = work @@ -146,11 +146,13 @@ class AbstractConnector(ABC): def create_book(self, remote_id, data, model): ''' create a work or edition from data ''' + print(remote_id) book = model.objects.create( remote_id=remote_id, title=data['title'], connector=self.connector, ) + print(book.remote_id) return self.update_book_from_data(book, data) @@ -161,7 +163,8 @@ class AbstractConnector(ABC): author_text = [] for author in self.get_authors_from_data(data): book.authors.add(author) - author_text.append(author.display_name) + if author.display_name: + author_text.append(author.display_name) book.author_text = ', '.join(author_text) book.save() @@ -215,6 +218,11 @@ class AbstractConnector(ABC): return None + @abstractmethod + def get_remote_id_from_data(self, data): + ''' otherwise we won't properly set the remote_id in the db ''' + + @abstractmethod def is_work_data(self, data): ''' differentiate works and editions ''' diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index a9517ca0..072910b6 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -47,8 +47,12 @@ class Connector(AbstractConnector): ] + def get_remote_id_from_data(self, data): + return data.get('id') + + def is_work_data(self, data): - return data['book_type'] == 'Work' + return data['type'] == 'Work' def get_edition_from_work_data(self, data): diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index 0ae3ce35..00b76f41 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -73,6 +73,14 @@ class Connector(AbstractConnector): + def get_remote_id_from_data(self, data): + try: + key = data['key'] + except KeyError: + 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'])) diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index 0e77ecf6..fb70978d 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -51,6 +51,9 @@ class Connector(AbstractConnector): ) + def get_remote_id_from_data(self, data): + pass + def is_work_data(self, data): pass diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 7507ee5b..882769bb 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -225,6 +225,15 @@ def handle_create(activity): if not reply: return + # look up books + book_urls = [] + if hasattr(activity, 'inReplyToBook'): + book_urls.append(activity.inReplyToBook) + if hasattr(activity, 'tag'): + book_urls += [t.href for t in activity.tag if t.type == 'Book'] + for remote_id in book_urls: + book = books_manager.get_or_create_book(remote_id) + model = models.activity_models[activity.type] status = activity.to_model(model) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 8798589f..36dbb06d 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -57,6 +57,17 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ''' structured replies block ''' return self.to_replies() + @property + def ap_tag(self): + tags = [] + for book in self.mention_books.all(): + tags.append(activitypub.Link( + href=book.local_id, + name=book.title, + type='Book' + )) + return tags + shared_mappings = [ ActivityMapping('id', 'remote_id'), ActivityMapping('url', 'remote_id'), @@ -66,6 +77,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ActivityMapping('to', 'ap_to'), ActivityMapping('cc', 'ap_cc'), ActivityMapping('replies', 'ap_replies'), + ActivityMapping('tag', 'ap_tag'), ] # serializing to bookwyrm expanded activitypub From 0393d8123073a3c130e0cc61a373152ebb4cd04f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 17:18:25 -0700 Subject: [PATCH 077/416] Fixes loading covers and authors --- bookwyrm/activitypub/base_activity.py | 1 - bookwyrm/connectors/abstract_connector.py | 2 -- bookwyrm/connectors/bookwyrm_connector.py | 8 ++++++-- bookwyrm/models/book.py | 9 +++++++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 1cffd0ed..fc5b1128 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -15,7 +15,6 @@ class ActivityEncoder(JSONEncoder): @dataclass class Image: ''' image block ''' - mediaType: str url: str type: str = 'Image' diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index c6e2f19e..9f9aed43 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -146,13 +146,11 @@ class AbstractConnector(ABC): def create_book(self, remote_id, data, model): ''' create a work or edition from data ''' - print(remote_id) book = model.objects.create( remote_id=remote_id, title=data['title'], connector=self.connector, ) - print(book.remote_id) return self.update_book_from_data(book, data) diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index 072910b6..c7a0f2ec 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -41,9 +41,13 @@ class Connector(AbstractConnector): ] self.author_mappings = [ - Mapping('born', remote_field='birth_date', formatter=get_date), - Mapping('died', remote_field='death_date', formatter=get_date), + Mapping('name'), Mapping('bio'), + Mapping('openlibrary_key'), + Mapping('wikipedia_link'), + Mapping('aliases'), + Mapping('born', formatter=get_date), + Mapping('died', formatter=get_date), ] diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 03b2c1f8..0920a931 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -58,6 +58,14 @@ class Book(ActivitypubMixin, BookWyrmModel): ''' the activitypub serialization should be a list of author ids ''' return [a.remote_id for a in self.authors.all()] + @property + def ap_cover(self): + ''' an image attachment ''' + return [activitypub.Image( + url='https://%s%s' % (DOMAIN, self.cover.url), + )] + + activity_mappings = [ ActivityMapping('id', 'remote_id'), @@ -90,6 +98,7 @@ class Book(ActivitypubMixin, BookWyrmModel): ActivityMapping('lccn', 'lccn'), ActivityMapping('editions', 'editions_path'), + ActivityMapping('attachment', 'ap_cover'), ] def save(self, *args, **kwargs): From b8e9f901387437c6ee583120a6294eb36f74f7d3 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 30 Oct 2020 17:41:32 -0700 Subject: [PATCH 078/416] no need to assign book var in incoming --- bookwyrm/incoming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 882769bb..56bea456 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -232,7 +232,7 @@ def handle_create(activity): if hasattr(activity, 'tag'): book_urls += [t.href for t in activity.tag if t.type == 'Book'] for remote_id in book_urls: - book = books_manager.get_or_create_book(remote_id) + books_manager.get_or_create_book(remote_id) model = models.activity_models[activity.type] status = activity.to_model(model) From 2cdd281e98509d5e38134e330a45d50c085bbb55 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 10:50:00 -0700 Subject: [PATCH 079/416] Prevent error on serializing book cover --- bookwyrm/models/book.py | 2 ++ bookwyrm/models/import_job.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 0920a931..b993ef5e 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -61,6 +61,8 @@ class Book(ActivitypubMixin, BookWyrmModel): @property def ap_cover(self): ''' an image attachment ''' + if not self.cover or not hasattr(self.cover, 'url'): + return [] return [activitypub.Image( url='https://%s%s' % (DOMAIN, self.cover.url), )] diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index f7b5e8a2..891bfd1b 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -72,7 +72,7 @@ class ImportItem(models.Model): def get_book_from_isbn(self): ''' search by isbn ''' search_result = books_manager.first_search_result( - self.isbn, min_confidence=0.992 + self.isbn, min_confidence=0.995 ) if search_result: try: @@ -90,7 +90,7 @@ class ImportItem(models.Model): self.data['Author'] ) search_result = books_manager.first_search_result( - search_term, min_confidence=0.992 + search_term, min_confidence=0.995 ) if search_result: try: From 301a452d9f1f73d8f7b8cb8b898ab465efa6cc71 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 10:39:42 -0700 Subject: [PATCH 080/416] Send Delete activity, not Tombstone on deletion --- bookwyrm/activitypub/verbs.py | 1 - bookwyrm/models/base_model.py | 14 ++++++++++++++ bookwyrm/outgoing.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index bd6d882d..eb166260 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -26,7 +26,6 @@ class Delete(Verb): ''' Create activity ''' to: List cc: List - signature: Signature type: str = 'Delete' diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 7d3cb344..8150d650 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -108,6 +108,20 @@ class ActivitypubMixin: ).serialize() + def to_delete_activity(self, user): + ''' notice of deletion ''' + # this should be a tombstone + activity_object = self.to_activity() + + return activitypub.Delete( + id=self.remote_id + '/activity', + actor=user.remote_id, + to=['%s/followers' % user.remote_id], + cc=['https://www.w3.org/ns/activitystreams#Public'], + object=activity_object, + ).serialize() + + def to_update_activity(self, user): ''' wrapper for Updates to an activity ''' activity_id = '%s#update/%s' % (user.remote_id, uuid4()) diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 908f3b5b..d236819e 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -204,7 +204,7 @@ def handle_imported_book(user, item, include_reviews, privacy): def handle_delete_status(user, status): ''' delete a status and broadcast deletion to other servers ''' delete_status(status) - broadcast(user, status.to_activity()) + broadcast(user, status.to_delete_activity(user)) def handle_status(user, form): From c4ed17474605619a84ecf007937236ea9da383dc Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 11:18:40 -0700 Subject: [PATCH 081/416] Adds flower for celery monitoring --- docker-compose.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 29ec83ee..640b2ecb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,6 +56,19 @@ services: - db - redis restart: on-failure + flower: + image: mher/flower + env_file: .env + environment: + - CELERY_BROKER_URL=${CELERY_BROKER} + networks: + - main + depends_on: + - db + - redis + restart: on-failure + ports: + - 8888:8888 volumes: pgdata: static_volume: From 02265b1e49ce99d46ff5ef34630343a3b5d3e10a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 12:45:39 -0700 Subject: [PATCH 082/416] Show federated servers and connectors in admin --- bookwyrm/admin.py | 2 + .../migrations/0062_auto_20201031_1936.py | 38 +++++++++++++++++++ bookwyrm/models/connector.py | 18 ++++++--- bookwyrm/templates/about.html | 10 +++-- 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 bookwyrm/migrations/0062_auto_20201031_1936.py diff --git a/bookwyrm/admin.py b/bookwyrm/admin.py index 2ea0a1d1..45af81d9 100644 --- a/bookwyrm/admin.py +++ b/bookwyrm/admin.py @@ -4,3 +4,5 @@ from bookwyrm import models admin.site.register(models.SiteSettings) admin.site.register(models.User) +admin.site.register(models.FederatedServer) +admin.site.register(models.Connector) diff --git a/bookwyrm/migrations/0062_auto_20201031_1936.py b/bookwyrm/migrations/0062_auto_20201031_1936.py new file mode 100644 index 00000000..2fbfe6c0 --- /dev/null +++ b/bookwyrm/migrations/0062_auto_20201031_1936.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.7 on 2020-10-31 19:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0061_auto_20201030_2157'), + ] + + operations = [ + migrations.AlterField( + 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', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='connector', + name='name', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='connector', + name='politeness_delay', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='connector', + name='search_url', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/bookwyrm/models/connector.py b/bookwyrm/models/connector.py index 2a9d496b..6f64cdf3 100644 --- a/bookwyrm/models/connector.py +++ b/bookwyrm/models/connector.py @@ -10,25 +10,25 @@ class Connector(BookWyrmModel): ''' 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) + 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 ) - api_key = models.CharField(max_length=255, null=True) + api_key = models.CharField(max_length=255, null=True, blank=True) base_url = models.CharField(max_length=255) books_url = models.CharField(max_length=255) covers_url = models.CharField(max_length=255) - search_url = models.CharField(max_length=255, null=True) + search_url = models.CharField(max_length=255, null=True, blank=True) - politeness_delay = models.IntegerField(null=True) #seconds - max_query_count = models.IntegerField(null=True) + 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) # when to reset the query count back to 0 (ie, after 1 day) - query_count_expiry = models.DateTimeField(auto_now_add=True) + query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True) class Meta: ''' check that there's code to actually use this connector ''' @@ -38,3 +38,9 @@ class Connector(BookWyrmModel): name='connector_file_valid' ) ] + + def __str__(self): + return "{} ({})".format( + self.identifier, + self.id, + ) diff --git a/bookwyrm/templates/about.html b/bookwyrm/templates/about.html index dbf6f852..4aae183a 100644 --- a/bookwyrm/templates/about.html +++ b/bookwyrm/templates/about.html @@ -3,14 +3,16 @@
- {% include 'snippets/about.html' with site_settings=site_settings %} +
+ {% include 'snippets/about.html' with site_settings=site_settings %} +

Code of Conduct

-

- {{ site_settings.code_of_conduct }} -

+
+ {{ site_settings.code_of_conduct }} +
{% endblock %} From 9ef03664f2255ebd270ffd44bc597121b86725fc Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 12:59:15 -0700 Subject: [PATCH 083/416] lookup books when resolving activity json --- bookwyrm/activitypub/base_activity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index fc5b1128..79987b50 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -2,6 +2,8 @@ from dataclasses import dataclass, fields, MISSING from json import JSONEncoder +from bookwyrm import books_manager, models + from django.db.models.fields.related_descriptors \ import ForwardManyToOneDescriptor @@ -102,6 +104,9 @@ class ActivityObject: def resolve_foreign_key(model, remote_id): ''' look up the remote_id on an activity json field ''' + if model in [models.Edition, models.Work]: + return books_manager.get_or_create_book(remote_id) + result = model.objects if hasattr(model.objects, 'select_subclasses'): result = result.select_subclasses() From 2463e643212dd725f07d6f702d8548aec22b29b2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 13:00:28 -0700 Subject: [PATCH 084/416] wrong quote in blockquote --- bookwyrm/static/css/format.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/format.css index b1bf20cd..3f16d94d 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/format.css @@ -108,7 +108,7 @@ input.toggle-control:checked ~ .toggle-content { position: absolute; } .quote blockquote:before { - content: "\e904"; + content: "\e905"; top: 0; left: 0; } From a7d8376b6af7e9fbb50b7bc7cae808e4a0505673 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 13:06:22 -0700 Subject: [PATCH 085/416] Small activitypub serialization issues --- bookwyrm/activitypub/person.py | 2 +- bookwyrm/models/user.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index dee6c1f1..118774a2 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -16,7 +16,7 @@ class Person(ActivityObject): publicKey: PublicKey endpoints: Dict icon: Image = field(default=lambda: {}) - bookwyrmUser: str = False + bookwyrmUser: bool = False manuallyApprovesFollowers: str = False discoverable: str = True type: str = 'Person' diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index d1e1f69e..38442ed7 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -82,12 +82,9 @@ class User(OrderedCollectionPageMixin, AbstractUser): ''' send default icon if one isn't set ''' if self.avatar: url = self.avatar.url - # TODO not the right way to get the media type - media_type = 'image/%s' % url.split('.')[-1] else: url = 'https://%s/static/images/default_avi.jpg' % DOMAIN - media_type = 'image/jpeg' - return activitypub.Image(media_type, url, 'Image') + return activitypub.Image(url=url) @property def ap_public_key(self): @@ -106,6 +103,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): activity_formatter=lambda x: x.split('@')[0] ), ActivityMapping('name', 'name'), + ActivityMapping('bookwyrmUser', 'bookwyrm_user'), ActivityMapping('inbox', 'inbox'), ActivityMapping('outbox', 'outbox'), ActivityMapping('followers', 'ap_followers'), From c45247e236268488dc0d90e212285e0b4e44c6d1 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 13:39:52 -0700 Subject: [PATCH 086/416] correctly grab book data from tags --- bookwyrm/incoming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 56bea456..30d741e8 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -230,7 +230,7 @@ def handle_create(activity): if hasattr(activity, 'inReplyToBook'): book_urls.append(activity.inReplyToBook) if hasattr(activity, 'tag'): - book_urls += [t.href for t in activity.tag if t.type == 'Book'] + book_urls += [t['href'] for t in activity.tag if t['type'] == 'Book'] for remote_id in book_urls: books_manager.get_or_create_book(remote_id) From 93baaf261a3c78c3fb3f43e67958cc4191eb7c4e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 14:18:56 -0700 Subject: [PATCH 087/416] Show/hide toggle for long book descriptions --- .../templates/snippets/book_description.html | 22 +++++++++++++++++-- bookwyrm/templatetags/fr_display.py | 19 ++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/snippets/book_description.html b/bookwyrm/templates/snippets/book_description.html index 12c9ccfb..839a8098 100644 --- a/bookwyrm/templates/snippets/book_description.html +++ b/bookwyrm/templates/snippets/book_description.html @@ -1,2 +1,20 @@ -
{% if book.description %}{{ book.description }}{% elif book.parent_work.description %}{{ book.parent_work.description }}{% endif %}
- +{% load fr_display %} +{% with book|book_description as full %} + {% with full|text_overflow as trimmed %} + {% if trimmed != full %} +
+ + +
+
+ + +
+ {% else %} +
{{ full }} +
+ {% endif %} + {% endwith %} +{% endwith %} diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py index cb4ee419..9e6e35cd 100644 --- a/bookwyrm/templatetags/fr_display.py +++ b/bookwyrm/templatetags/fr_display.py @@ -106,6 +106,25 @@ def get_edition_info(book): return ', '.join(i for i in items if i) +@register.filter(name='book_description') +def get_book_description(book): + ''' use the work's text if the book doesn't have it ''' + return book.description or book.parent_work.description + + +@register.filter(name='text_overflow') +def text_overflow(text): + ''' dont' let book descriptions run for ages ''' + char_max = 500 + if len(text) < char_max: + return text + + trimmed = text[:char_max] + # go back to the last space + trimmed = ' '.join(trimmed.split(' ')[:-1]) + return trimmed + '...' + + @register.simple_tag(takes_context=True) def shelve_button_identifier(context, book): ''' check what shelf a user has a book on, if any ''' From 4684a83e6f4316d346371aede2e9b4b60c2c44a7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 31 Oct 2020 21:56:45 -0700 Subject: [PATCH 088/416] fixes quote character --- bookwyrm/static/css/format.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/format.css index 3f16d94d..bd13143b 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/format.css @@ -113,7 +113,7 @@ input.toggle-control:checked ~ .toggle-content { left: 0; } .quote blockquote:after { - content: "\e903"; + content: "\e904"; right: 0; } From c33445121629e3f2c22d1715723b34f6fafbd299 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 08:54:10 -0800 Subject: [PATCH 089/416] code cleanup --- bookwyrm/models/status.py | 5 +++-- bookwyrm/models/user.py | 43 ++++++++++++++++++++------------------- bookwyrm/urls.py | 2 +- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 36dbb06d..3915dda2 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -59,6 +59,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @property def ap_tag(self): + ''' books or (eventually) users tagged in a post ''' tags = [] for book in self.mention_books.all(): tags.append(activitypub.Link( @@ -117,7 +118,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): **kwargs ) - def to_activity(self, **kwargs): + def to_activity(self, pure=False): ''' return tombstone if the status is deleted ''' if self.deleted: return activitypub.Tombstone( @@ -126,7 +127,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): deleted=self.deleted_date.isoformat(), published=self.deleted_date.isoformat() ).serialize() - return ActivitypubMixin.to_activity(self, **kwargs) + return ActivitypubMixin.to_activity(self, pure=pure) class GeneratedNote(Status): diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 38442ed7..973d4387 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -167,28 +167,29 @@ class User(OrderedCollectionPageMixin, AbstractUser): return activity_object -@receiver(models.signals.pre_save, sender=User) -def execute_before_save(sender, instance, *args, **kwargs): - ''' populate fields for new local users ''' - # this user already exists, no need to poplate fields - if instance.id: - return - if not instance.local: - # we need to generate a username that uses the domain (webfinger format) - actor_parts = urlparse(instance.remote_id) - instance.username = '%s@%s' % (instance.username, actor_parts.netloc) - return + def save(self, *args, **kwargs): + ''' populate fields for new local users ''' + # this user already exists, no need to populate fields + if self.id: + return + if not self.local: + # generate a username that uses the domain (webfinger format) + actor_parts = urlparse(self.remote_id) + self.username = '%s@%s' % (self.username, actor_parts.netloc) + return - # populate fields for local users - instance.remote_id = 'https://%s/user/%s' % (DOMAIN, instance.username) - instance.localname = instance.username - instance.username = '%s@%s' % (instance.username, DOMAIN) - instance.actor = instance.remote_id - instance.inbox = '%s/inbox' % instance.remote_id - instance.shared_inbox = 'https://%s/inbox' % DOMAIN - instance.outbox = '%s/outbox' % instance.remote_id - if not instance.private_key: - instance.private_key, instance.public_key = create_key_pair() + # populate fields for local users + self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.username) + self.localname = self.username + self.username = '%s@%s' % (self.username, DOMAIN) + self.actor = self.remote_id + self.inbox = '%s/inbox' % self.remote_id + self.shared_inbox = 'https://%s/inbox' % DOMAIN + self.outbox = '%s/outbox' % self.remote_id + if not self.private_key: + self.private_key, self.public_key = create_key_pair() + + super().save(*args, **kwargs) @receiver(models.signals.post_save, sender=User) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 19fdf2e4..78c99ae4 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -17,7 +17,7 @@ status_types = [ 'comment', 'quotation', 'boost', - 'generatedstatus' + 'generatednote' ] status_path = r'%s/(%s)/(?P\d+)' % \ (local_user_path, '|'.join(status_types)) From 4e02a8df998c758b9dd591aaeaa3d0f4e73076f0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 09:16:49 -0800 Subject: [PATCH 090/416] Track when user was last active fixes #10 --- .../migrations/0063_user_last_active_date.py | 18 ++++++++++++++++++ bookwyrm/models/status.py | 15 +++++++++++++++ bookwyrm/models/user.py | 2 ++ bookwyrm/view_actions.py | 6 +++++- bookwyrm/wellknown.py | 17 +++++++++++++++-- 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 bookwyrm/migrations/0063_user_last_active_date.py diff --git a/bookwyrm/migrations/0063_user_last_active_date.py b/bookwyrm/migrations/0063_user_last_active_date.py new file mode 100644 index 00000000..0cecc37d --- /dev/null +++ b/bookwyrm/migrations/0063_user_last_active_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-11-01 17:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0062_auto_20201031_1936'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='last_active_date', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 3915dda2..5797935e 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -129,6 +129,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ).serialize() return ActivitypubMixin.to_activity(self, pure=pure) + def save(self, *args, **kwargs): + self.user.last_active_date = timezone.now() + self.user.save() + super().save(*args, **kwargs) + class GeneratedNote(Status): ''' these are app-generated messages about user activity ''' @@ -228,6 +233,11 @@ class Favorite(ActivitypubMixin, BookWyrmModel): activity_serializer = activitypub.Like + def save(self, *args, **kwargs): + self.user.last_active_date = timezone.now() + self.user.save() + super().save(*args, **kwargs) + class Meta: ''' can't fav things twice ''' @@ -268,6 +278,11 @@ class ReadThrough(BookWyrmModel): blank=True, null=True) + def save(self, *args, **kwargs): + self.user.last_active_date = timezone.now() + self.user.save() + super().save(*args, **kwargs) + NotificationType = models.TextChoices( 'NotificationType', diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 973d4387..bebd5540 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -69,6 +69,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): remote_id = models.CharField(max_length=255, null=True, unique=True) 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 = models.BooleanField(default=False) # ---- activitypub serialization settings for this model ----- # @@ -172,6 +173,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): # this user already exists, no need to populate fields if self.id: return + if not self.local: # generate a username that uses the domain (webfinger format) actor_parts = urlparse(self.remote_id) diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 4fca1685..e9a1171d 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -4,13 +4,15 @@ from PIL import Image import dateutil.parser from dateutil.parser import ParserError + from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required, permission_required +from django.core.exceptions import PermissionDenied from django.core.files.base import ContentFile from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import redirect from django.template.response import TemplateResponse -from django.core.exceptions import PermissionDenied +from django.utils import timezone from bookwyrm import books_manager from bookwyrm import forms, models, outgoing @@ -32,7 +34,9 @@ def user_login(request): 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() return redirect(request.GET.get('next', '/')) login_form.non_field_errors = 'Username or password are incorrect' diff --git a/bookwyrm/wellknown.py b/bookwyrm/wellknown.py index b59256fc..e61ec358 100644 --- a/bookwyrm/wellknown.py +++ b/bookwyrm/wellknown.py @@ -1,4 +1,7 @@ ''' responds to various requests to /.well-know ''' +from datetime import datetime + +from dateutil.relativedelta import relativedelta from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import JsonResponse @@ -52,6 +55,16 @@ def nodeinfo(request): status_count = models.Status.objects.filter(user__local=True).count() user_count = models.User.objects.count() + + month_ago = datetime.now() - relativedelta(months=1) + last_month_count = models.User.objects.filter( + last_active_date__gt=month_ago + ).count() + + six_months_ago = datetime.now() - relativedelta(months=6) + six_month_count = models.User.objects.filter( + last_active_date__gt=six_months_ago + ).count() return JsonResponse({ 'version': '2.0', 'software': { @@ -64,8 +77,8 @@ def nodeinfo(request): 'usage': { 'users': { 'total': user_count, - 'activeMonth': user_count, # TODO - 'activeHalfyear': user_count, # TODO + 'activeMonth': last_month_count, + 'activeHalfyear': six_month_count, }, 'localPosts': status_count, }, From 0cf1838276b1ce1c02e2e1dd8bfa5a29de3b4180 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 10:13:51 -0800 Subject: [PATCH 091/416] Mention and notify users when creating a status --- bookwyrm/activitypub/__init__.py | 3 +- bookwyrm/activitypub/base_activity.py | 13 +++++++ bookwyrm/activitypub/note.py | 11 ++---- .../migrations/0063_auto_20201101_1758.py | 26 ++++++++++++++ bookwyrm/models/status.py | 8 ++++- bookwyrm/outgoing.py | 34 ++++++++++++++++++- 6 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 bookwyrm/migrations/0063_auto_20201101_1758.py diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index c10d1ca1..105e889b 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -3,8 +3,9 @@ import inspect import sys from .base_activity import ActivityEncoder, Image, PublicKey, Signature +from .base_activity import Link, Mention from .note import Note, GeneratedNote, Article, Comment, Review, Quotation -from .note import Tombstone, Link +from .note import Tombstone from .interaction import Boost, Like from .ordered_collection import OrderedCollection, OrderedCollectionPage from .person import Person diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 79987b50..54cc4679 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -21,6 +21,19 @@ class Image: type: str = 'Image' +@dataclass +class Link(): + ''' for tagging a book in a status ''' + href: str + name: str + type: str = 'Link' + +@dataclass +class Mention(Link): + ''' a subtype of Link for mentioning an actor ''' + type: str = 'Mention' + + @dataclass class PublicKey: ''' public key block ''' diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index d187acc6..357e164f 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from typing import Dict, List -from .base_activity import ActivityObject, Image +from .base_activity import ActivityObject, Image, Link @dataclass(init=False) class Tombstone(ActivityObject): @@ -20,6 +20,7 @@ class Note(ActivityObject): inReplyTo: str published: str attributedTo: str + tag: List[Link] to: List[str] cc: List[str] content: str @@ -36,17 +37,9 @@ class Article(Note): type: str = 'Article' -@dataclass -class Link(): - ''' for tagging a book in a status ''' - href: str - name: str - type: str = 'Link' - @dataclass(init=False) class GeneratedNote(Note): ''' just a re-typed note ''' - tag: List[Link] type: str = 'GeneratedNote' diff --git a/bookwyrm/migrations/0063_auto_20201101_1758.py b/bookwyrm/migrations/0063_auto_20201101_1758.py new file mode 100644 index 00000000..758548f7 --- /dev/null +++ b/bookwyrm/migrations/0063_auto_20201101_1758.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.7 on 2020-11-01 17:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0062_auto_20201031_1936'), + ] + + operations = [ + migrations.RemoveConstraint( + 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), + ), + 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'), + ), + ] diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 36dbb06d..31c77fa2 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -59,6 +59,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @property def ap_tag(self): + ''' references to books and/or users ''' tags = [] for book in self.mention_books.all(): tags.append(activitypub.Link( @@ -66,6 +67,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): name=book.title, type='Book' )) + for user in self.mention_users.all(): + tags.append(activitypub.Mention( + href=user.remote_id, + name=user.username, + )) return tags shared_mappings = [ @@ -270,7 +276,7 @@ class ReadThrough(BookWyrmModel): NotificationType = models.TextChoices( 'NotificationType', - 'FAVORITE REPLY TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT') + 'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT') class Notification(BookWyrmModel): ''' you've been tagged, liked, followed, etc ''' diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index d236819e..68d79013 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -1,5 +1,6 @@ ''' handles all the activity coming out of the server ''' from datetime import datetime +import re from django.db import IntegrityError, transaction from django.http import HttpResponseNotFound, JsonResponse @@ -13,6 +14,7 @@ from bookwyrm.status import create_tag, create_notification from bookwyrm.status import create_generated_note from bookwyrm.status import delete_status from bookwyrm.remote_user import get_or_create_remote_user +from bookwyrm.settings import DOMAIN @csrf_exempt @@ -211,7 +213,37 @@ def handle_status(user, form): ''' generic handler for statuses ''' status = form.save() - # notify reply parent or (TODO) tagged users + # inspect the text for user tags + text = status.content + matches = re.finditer( + r'\W@[a-zA-Z_\-\.0-9]+(@[a-z-A-Z0-9_\-]+.[a-z]+)?', + text + ) + for match in matches: + 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) + + try: + mention_user = models.User.objects.get(username=username) + except models.User.DoesNotExist: + # we can ignore users we don't know about + continue + # add them to status mentions fk + status.mention_users.add(mention_user) + # create notification if the mentioned user is local + if mention_user.local: + create_notification( + mention_user, + 'MENTION', + related_user=user, + related_status=status + ) + status.save() + + # notify reply parent or tagged users if status.reply_parent and status.reply_parent.user.local: create_notification( status.reply_parent.user, From 29094f3c3f28f595a9b150e3fa3daa4fed9d49f8 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 10:15:56 -0800 Subject: [PATCH 092/416] Notification text for mentions --- bookwyrm/templates/notifications.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index e59810b9..afd4d8ae 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -22,6 +22,10 @@ favorited your status + {% elif notification.notification_type == 'MENTION' %} + mentioned you in a + status + {% elif notification.notification_type == 'REPLY' %} replied to your From 203a0a25ebddc170d9594961c0fb9095c0a6e4ee Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 10:42:48 -0800 Subject: [PATCH 093/416] Fixes webfinger --- bookwyrm/outgoing.py | 20 ++++++++++++-------- bookwyrm/utils/__init__.py | 1 + bookwyrm/views.py | 3 ++- bookwyrm/wellknown.py | 13 ++++++++----- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 68d79013..95b89d9f 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -15,6 +15,7 @@ from bookwyrm.status import create_generated_note from bookwyrm.status import delete_status from bookwyrm.remote_user import get_or_create_remote_user from bookwyrm.settings import DOMAIN +from bookwyrm.utils import regex @csrf_exempt @@ -36,13 +37,17 @@ def outbox(request, username): def handle_remote_webfinger(query): - ''' webfingerin' other servers ''' + ''' webfingerin' other servers, username query should be user@domain ''' user = None - domain = query.split('@')[1] + try: + domain = query.split('@')[2] + 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' % \ + url = 'https://%s/.well-known/webfinger?resource=acct:@%s' % \ (domain, query) try: response = requests.get(url) @@ -57,7 +62,7 @@ def handle_remote_webfinger(query): user = get_or_create_remote_user(link['href']) except KeyError: return None - return [user] + return user def handle_follow(user, to_follow): @@ -216,7 +221,7 @@ def handle_status(user, form): # inspect the text for user tags text = status.content matches = re.finditer( - r'\W@[a-zA-Z_\-\.0-9]+(@[a-z-A-Z0-9_\-]+.[a-z]+)?', + regex.username, text ) for match in matches: @@ -226,9 +231,8 @@ def handle_status(user, form): username.append(DOMAIN) username = '@'.join(username) - try: - mention_user = models.User.objects.get(username=username) - except models.User.DoesNotExist: + mention_user = handle_remote_webfinger(username) + if not mention_user: # we can ignore users we don't know about continue # add them to status mentions fk diff --git a/bookwyrm/utils/__init__.py b/bookwyrm/utils/__init__.py index e69de29b..a90554c7 100644 --- a/bookwyrm/utils/__init__.py +++ b/bookwyrm/utils/__init__.py @@ -0,0 +1 @@ +from .regex import username diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 4721644c..4666409d 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -16,6 +16,7 @@ from bookwyrm.activitypub import ActivityEncoder from bookwyrm import forms, models, books_manager from bookwyrm import goodreads_import from bookwyrm.tasks import app +from bookwyrm.utils import regex def get_user_from_username(username): @@ -168,7 +169,7 @@ def search(request): return JsonResponse([r.__dict__ for r in book_results], safe=False) # use webfinger looks like a mastodon style account@domain.com username - if re.match(r'\w+@\w+.\w+', query): + if re.match(regex.full_username, query): outgoing.handle_remote_webfinger(query) # do a local user search diff --git a/bookwyrm/wellknown.py b/bookwyrm/wellknown.py index b59256fc..94320c2d 100644 --- a/bookwyrm/wellknown.py +++ b/bookwyrm/wellknown.py @@ -1,5 +1,5 @@ ''' responds to various requests to /.well-know ''' -from django.http import HttpResponseBadRequest, HttpResponseNotFound +from django.http import HttpResponseNotFound from django.http import JsonResponse from bookwyrm import models @@ -13,11 +13,14 @@ def webfinger(request): resource = request.GET.get('resource') if not resource and not resource.startswith('acct:'): - return HttpResponseBadRequest() - ap_id = resource.replace('acct:', '') - user = models.User.objects.filter(username=ap_id).first() - if not user: + return HttpResponseNotFound() + + username = resource.replace('acct:@', '') + try: + user = models.User.objects.get(username=username) + except models.User.DoesNotExist: return HttpResponseNotFound('No account found') + return JsonResponse({ 'subject': 'acct:%s' % (user.username), 'links': [ From b941bb7ad5054295d45e7259eee50c85b12a5e07 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 10:53:47 -0800 Subject: [PATCH 094/416] format webfinger "subject" --- bookwyrm/wellknown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/wellknown.py b/bookwyrm/wellknown.py index 94320c2d..f0fc6b76 100644 --- a/bookwyrm/wellknown.py +++ b/bookwyrm/wellknown.py @@ -22,7 +22,7 @@ def webfinger(request): return HttpResponseNotFound('No account found') return JsonResponse({ - 'subject': 'acct:%s' % (user.username), + 'subject': 'acct:@%s' % (user.username), 'links': [ { 'rel': 'self', From 85a7e83340762c78433cd44a7972e7e1482b7e9a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 10:57:17 -0800 Subject: [PATCH 095/416] Adds regex util file --- bookwyrm/utils/regex.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 bookwyrm/utils/regex.py diff --git a/bookwyrm/utils/regex.py b/bookwyrm/utils/regex.py new file mode 100644 index 00000000..36e211d9 --- /dev/null +++ b/bookwyrm/utils/regex.py @@ -0,0 +1,5 @@ +''' defining regexes for regularly used concepts ''' + +domain = r'[a-z-A-Z0-9_\-]+\.[a-z]+' +username = r'@[a-zA-Z_\-\.0-9]+(@%s)?' % domain +full_username = r'@[a-zA-Z_\-\.0-9]+@%s' % domain From 76a4f0e9a7ac10ec5e2b7edd9cde7179a386b476 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 11:13:34 -0800 Subject: [PATCH 096/416] Merge migration --- bookwyrm/migrations/0064_merge_20201101_1913.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 bookwyrm/migrations/0064_merge_20201101_1913.py diff --git a/bookwyrm/migrations/0064_merge_20201101_1913.py b/bookwyrm/migrations/0064_merge_20201101_1913.py new file mode 100644 index 00000000..d6d97367 --- /dev/null +++ b/bookwyrm/migrations/0064_merge_20201101_1913.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.7 on 2020-11-01 19:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0063_user_last_active_date'), + ('bookwyrm', '0063_auto_20201101_1758'), + ] + + operations = [ + ] From fdaa63d5dc4e46685dc7e7d38bc0004d28f06db4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 11:57:46 -0800 Subject: [PATCH 097/416] Fixes error in text trimming tag --- bookwyrm/templatetags/fr_display.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py index 9e6e35cd..0595ff52 100644 --- a/bookwyrm/templatetags/fr_display.py +++ b/bookwyrm/templatetags/fr_display.py @@ -115,8 +115,10 @@ def get_book_description(book): @register.filter(name='text_overflow') def text_overflow(text): ''' dont' let book descriptions run for ages ''' + if not text: + return char_max = 500 - if len(text) < char_max: + if text and len(text) < char_max: return text trimmed = text[:char_max] From beb5e1f11e75b64e445c850ebbe9b5065a679cbb Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 11:59:51 -0800 Subject: [PATCH 098/416] Show empty string, not "None" for books with no text --- bookwyrm/templatetags/fr_display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py index 0595ff52..52cb3c47 100644 --- a/bookwyrm/templatetags/fr_display.py +++ b/bookwyrm/templatetags/fr_display.py @@ -116,7 +116,7 @@ def get_book_description(book): def text_overflow(text): ''' dont' let book descriptions run for ages ''' if not text: - return + return '' char_max = 500 if text and len(text) < char_max: return text From a2692f92d24ae8382e95999053e6d459a10e0369 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 1 Nov 2020 12:07:51 -0800 Subject: [PATCH 099/416] Fixes logic issues in saving user model --- bookwyrm/models/user.py | 6 +++--- bookwyrm/outgoing.py | 9 +++++++-- bookwyrm/templates/layout.html | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index bebd5540..6043faf4 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -172,13 +172,13 @@ class User(OrderedCollectionPageMixin, AbstractUser): ''' populate fields for new local users ''' # this user already exists, no need to populate fields if self.id: - return + return super().save(*args, **kwargs) if not self.local: # generate a username that uses the domain (webfinger format) actor_parts = urlparse(self.remote_id) self.username = '%s@%s' % (self.username, actor_parts.netloc) - return + return super().save(*args, **kwargs) # populate fields for local users self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.username) @@ -191,7 +191,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): if not self.private_key: self.private_key, self.public_key = create_key_pair() - super().save(*args, **kwargs) + return super().save(*args, **kwargs) @receiver(models.signals.post_save, sender=User) diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 95b89d9f..e9daa8fa 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -37,10 +37,15 @@ def outbox(request, username): def handle_remote_webfinger(query): - ''' webfingerin' other servers, username query should be user@domain ''' + ''' webfingerin' other servers ''' user = None + + # usernames could be @user@domain or user@domain + if query[0] == '@': + query = query[1:] + try: - domain = query.split('@')[2] + domain = query.split('@')[1] except IndexError: return None diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 55380652..dfe48327 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -27,7 +27,7 @@
- + - -
+
  • + {% include 'snippets/search_result_text.html' with result=result link=True %} +
  • {% endfor %} + {% endwith %} + - {% endif %} - {% endfor %} - {% if not book_results %} -

    No books found for "{{ query }}"

    - {% endif %} + +
    +

    + Didn't find what you were looking for? +

    + + + +
    + + +

    Matching Users

    From 485f3831b912cd3d7a5005bf6f1ec8fbb53eea65 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 2 Nov 2020 08:50:21 -0800 Subject: [PATCH 105/416] Priortize other instances over openlibrary --- bookwyrm/books_manager.py | 2 +- init_db.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/books_manager.py b/bookwyrm/books_manager.py index 37a31766..461017a0 100644 --- a/bookwyrm/books_manager.py +++ b/bookwyrm/books_manager.py @@ -50,7 +50,7 @@ def get_or_create_connector(remote_id): books_url='https://%s/book' % identifier, covers_url='https://%s/images/covers' % identifier, search_url='https://%s/search?q=' % identifier, - priority=3 + priority=2 ) return load_connector(connector_info) diff --git a/init_db.py b/init_db.py index ef11f8c5..a7a45c20 100644 --- a/init_db.py +++ b/init_db.py @@ -76,4 +76,5 @@ Connector.objects.create( books_url='https://openlibrary.org', covers_url='https://covers.openlibrary.org', search_url='https://openlibrary.org/search?q=', + priority=3, ) From 903e68f64a5c4eadcbfc6343ee3da5bd9dbbe81a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 2 Nov 2020 09:03:48 -0800 Subject: [PATCH 106/416] Show extended search results automatically for empty local results --- bookwyrm/templates/search_results.html | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html index 6c712349..14e5fbbd 100644 --- a/bookwyrm/templates/search_results.html +++ b/bookwyrm/templates/search_results.html @@ -1,20 +1,24 @@ {% extends 'layout.html' %} {% block content %} +{% with book_results|first as local_results %}

    Matching Books

    + {% if not local_results.results %} +

    No books found for "{{ query }}"

    + {% else %} + {% endif %}
    + {% if book_results|slice:":1" and local_results.results %}

    Didn't find what you were looking for? @@ -25,8 +29,9 @@

    + {% endif %} - +
    @@ -72,4 +76,5 @@ {% endfor %}
    +{% endwith %} {% endblock %} From 30d5846fa4af28b882ec74135029286b2b192e19 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 2 Nov 2020 09:23:47 -0800 Subject: [PATCH 107/416] Adds missing template snippet --- bookwyrm/templates/snippets/search_result_text.html | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 bookwyrm/templates/snippets/search_result_text.html diff --git a/bookwyrm/templates/snippets/search_result_text.html b/bookwyrm/templates/snippets/search_result_text.html new file mode 100644 index 00000000..183b1ec8 --- /dev/null +++ b/bookwyrm/templates/snippets/search_result_text.html @@ -0,0 +1,2 @@ +{% if link %}{{ result.title }}{% else %}{{ result.title }}{% endif %} +{% if result.author %} by {{ result.author }}{% endif %}{% if result.year %} ({{ result.year }}){% endif %} From 2ac9a6fceea8938682644227ceded8db76e552a4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 2 Nov 2020 09:34:46 -0800 Subject: [PATCH 108/416] Fixes updating books from remote instances --- bookwyrm/connectors/abstract_connector.py | 6 +++--- bookwyrm/incoming.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 9f9aed43..1faa9bf9 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -1,13 +1,13 @@ ''' functionality outline for a book data connector ''' from abc import ABC, abstractmethod from dataclasses import dataclass -from dateutil import parser import pytz -import requests -from requests import HTTPError from urllib3.exceptions import RequestError from django.db import transaction +from dateutil import parser +import requests +from requests import HTTPError from bookwyrm import models diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 30d741e8..5f4cc15b 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -69,7 +69,8 @@ def shared_inbox(request): }, 'Update': { 'Person': handle_update_user, - 'Document': handle_update_book, + 'Edition': handle_update_book, + 'Work': handle_update_book, }, } activity_type = activity['type'] @@ -337,7 +338,7 @@ def handle_update_book(activity): document = activity['object'] # check if we have their copy and care about their updates book = models.Book.objects.select_subclasses().filter( - remote_id=document['url'], + remote_id=document['id'], sync=True, ).first() if not book: From b5467f7d6d166e2b32adcde896496a80098b46b4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 2 Nov 2020 11:46:23 -0800 Subject: [PATCH 109/416] Refactor status display --- bookwyrm/templates/book_results.html | 27 ------- .../templates/snippets/book_description.html | 10 ++- bookwyrm/templates/snippets/book_preview.html | 12 +++ bookwyrm/templates/snippets/interaction.html | 12 +-- bookwyrm/templates/snippets/status.html | 19 ++++- .../templates/snippets/status_content.html | 79 ++++++------------- .../templates/snippets/status_header.html | 45 ++++++----- bookwyrm/templates/user_results.html | 18 ----- bookwyrm/templatetags/fr_display.py | 7 +- bookwyrm/views.py | 2 +- 10 files changed, 93 insertions(+), 138 deletions(-) delete mode 100644 bookwyrm/templates/book_results.html create mode 100644 bookwyrm/templates/snippets/book_preview.html delete mode 100644 bookwyrm/templates/user_results.html diff --git a/bookwyrm/templates/book_results.html b/bookwyrm/templates/book_results.html deleted file mode 100644 index 71d3a637..00000000 --- a/bookwyrm/templates/book_results.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'layout.html' %} -{% block content %} -
    -

    Search results

    - {% for result_set in results %} - {% if result_set.results %} -
    - {% if not result_set.connector.local %} -

    - Results from {% if result_set.connector.name %}{{ result_set.connector.name }}{% else %}{{ result_set.connector.identifier }}{% endif %} -

    - {% endif %} - - {% for result in result_set.results %} -
    -
    - {% csrf_token %} - - - -
    - {% endfor %} -
    - {% endif %} - {% endfor %} -
    -{% endblock %} diff --git a/bookwyrm/templates/snippets/book_description.html b/bookwyrm/templates/snippets/book_description.html index 21ed73aa..82f8db37 100644 --- a/bookwyrm/templates/snippets/book_description.html +++ b/bookwyrm/templates/snippets/book_description.html @@ -1,17 +1,18 @@ {% load fr_display %} +{% with book.id|uuid as uuid %} {% with book|book_description as full %} {% if full %} {% with full|text_overflow as trimmed %} {% if trimmed != full %}
    - + +
    - + +
    {% else %}
    {{ full }} @@ -20,3 +21,4 @@ {% endwith %} {% endif %} {% endwith %} +{% endwith %} diff --git a/bookwyrm/templates/snippets/book_preview.html b/bookwyrm/templates/snippets/book_preview.html new file mode 100644 index 00000000..65a9ae4b --- /dev/null +++ b/bookwyrm/templates/snippets/book_preview.html @@ -0,0 +1,12 @@ +
    +
    +
    + {% include 'snippets/book_cover.html' with book=book %} + {% include 'snippets/shelve_button.html' with book=book %} +
    +
    +
    +

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

    + {% include 'snippets/book_description.html' with book=book %} +
    +
    diff --git a/bookwyrm/templates/snippets/interaction.html b/bookwyrm/templates/snippets/interaction.html index a48d8a7c..97e7d068 100644 --- a/bookwyrm/templates/snippets/interaction.html +++ b/bookwyrm/templates/snippets/interaction.html @@ -1,4 +1,5 @@ {% load fr_display %} +{% with activity.id|uuid as uuid %} -
    + {% csrf_token %} -
    + {% csrf_token %} -
    + {% csrf_token %} -
    + {% csrf_token %} - - - -
    - {% csrf_token %} - - -
    - {% csrf_token %} - - - -
    - {% csrf_token %} - - -
    - {% csrf_token %} - - - {% else %} - - - Comment - - - - Boost status - - - - Like status - - - {% endif %} - -{% endwith %} diff --git a/bookwyrm/templates/snippets/status.html b/bookwyrm/templates/snippets/status.html index 4c3284c6..6e249520 100644 --- a/bookwyrm/templates/snippets/status.html +++ b/bookwyrm/templates/snippets/status.html @@ -28,43 +28,101 @@ {% endif %} -
    - {% if status.status_type == 'Boost' %} - {% include 'snippets/interaction.html' with activity=status|boosted_status %} - {% else %} - {% include 'snippets/interaction.html' with activity=status %} +
    + {% if request.user.is_authenticated %} + + {% endif %} +
    diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py index 053058eb..2af66a54 100644 --- a/bookwyrm/templatetags/fr_display.py +++ b/bookwyrm/templatetags/fr_display.py @@ -117,7 +117,7 @@ def text_overflow(text): ''' dont' let book descriptions run for ages ''' if not text: return '' - char_max = 500 + char_max = 400 if text and len(text) < char_max: return text @@ -129,6 +129,7 @@ def text_overflow(text): @register.filter(name='uuid') def get_uuid(identifier): + ''' for avoiding clashing ids when there are many forms ''' return '%s%s' % (identifier, uuid4()) From a3bf31796aa36756471e2e31212714359dad7fcd Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 5 Nov 2020 11:40:03 -0800 Subject: [PATCH 136/416] Make status footer more mobile friendly --- bookwyrm/templates/snippets/boost_button.html | 19 +++++++ bookwyrm/templates/snippets/fav_button.html | 19 +++++++ bookwyrm/templates/snippets/reply_form.html | 33 +++++++++++++ bookwyrm/templates/snippets/status.html | 49 ++++++++++--------- bookwyrm/templatetags/fr_display.py | 24 +++++++++ 5 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 bookwyrm/templates/snippets/boost_button.html create mode 100644 bookwyrm/templates/snippets/fav_button.html create mode 100644 bookwyrm/templates/snippets/reply_form.html diff --git a/bookwyrm/templates/snippets/boost_button.html b/bookwyrm/templates/snippets/boost_button.html new file mode 100644 index 00000000..7133e818 --- /dev/null +++ b/bookwyrm/templates/snippets/boost_button.html @@ -0,0 +1,19 @@ +{% load fr_display %} +{% with activity.id|uuid as uuid %} +
    + {% csrf_token %} + + +
    + {% csrf_token %} + + +{% endwith %} diff --git a/bookwyrm/templates/snippets/fav_button.html b/bookwyrm/templates/snippets/fav_button.html new file mode 100644 index 00000000..de41064a --- /dev/null +++ b/bookwyrm/templates/snippets/fav_button.html @@ -0,0 +1,19 @@ +{% load fr_display %} +{% with activity.id|uuid as uuid %} +
    + {% csrf_token %} + + +
    + {% csrf_token %} + + +{% endwith %} diff --git a/bookwyrm/templates/snippets/reply_form.html b/bookwyrm/templates/snippets/reply_form.html new file mode 100644 index 00000000..48371f63 --- /dev/null +++ b/bookwyrm/templates/snippets/reply_form.html @@ -0,0 +1,33 @@ +{% load fr_display %} +{% with activity.id|uuid as uuid %} +
    +
    + {% csrf_token %} + + +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +{% endwith %} diff --git a/bookwyrm/templates/snippets/status.html b/bookwyrm/templates/snippets/status.html index 6e249520..9390d39c 100644 --- a/bookwyrm/templates/snippets/status.html +++ b/bookwyrm/templates/snippets/status.html @@ -47,10 +47,8 @@ - {{ status.published_date | naturaltime }} - - {% if status.user == request.user %} + + {% if status.user == request.user %} + - - - - + +
    + + +
    {% else %} diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py index 2af66a54..73df1db8 100644 --- a/bookwyrm/templatetags/fr_display.py +++ b/bookwyrm/templatetags/fr_display.py @@ -1,6 +1,10 @@ ''' template filters ''' from uuid import uuid4 +from datetime import datetime + +from dateutil.relativedelta import relativedelta from django import template +from django.utils import timezone from bookwyrm import models @@ -133,6 +137,26 @@ def get_uuid(identifier): return '%s%s' % (identifier, uuid4()) +@register.filter(name="post_date") +def time_since(date): + ''' concise time ago function ''' + if not isinstance(date, datetime): + return '' + now = timezone.now() + delta = now - date + + if date < (now - relativedelta(weeks=1)): + return date.strftime('%b %-d') + delta = relativedelta(now, date) + if delta.days: + return '%dd' % delta.days + if delta.hours: + return '%dh' % delta.hours + if delta.minutes: + return '%dm' % delta.minutes + return '%ds' % delta.seconds + + @register.simple_tag(takes_context=True) def shelve_button_identifier(context, book): ''' check what shelf a user has a book on, if any ''' From a48bb5a16ea631362734812aa1c07f095afd8685 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 5 Nov 2020 12:05:29 -0800 Subject: [PATCH 137/416] Adds cancel button to edit book page --- bookwyrm/templates/book.html | 2 +- bookwyrm/templates/edit_book.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index 637df52b..d271f9c1 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -11,7 +11,7 @@ {% if request.user.is_authenticated and perms.bookwyrm.edit_book %} From 632ef258b7cd82f910103aee125a8e22a9c6712d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 5 Nov 2020 12:09:05 -0800 Subject: [PATCH 138/416] Colors for follow/unfollow buttons --- bookwyrm/templates/snippets/follow_button.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/snippets/follow_button.html b/bookwyrm/templates/snippets/follow_button.html index 5eea5bbf..1f914a6c 100644 --- a/bookwyrm/templates/snippets/follow_button.html +++ b/bookwyrm/templates/snippets/follow_button.html @@ -11,14 +11,14 @@ Follow request already sent. {% csrf_token %} {% if user.manually_approves_followers %} - + {% else %} - + {% endif %} {% csrf_token %} - + {% endif %} From 7612df5161fe0bbde00769aa34243591e7c63c00 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 5 Nov 2020 13:51:16 -0800 Subject: [PATCH 139/416] Use html in code of conduct --- bookwyrm/templates/about.html | 10 ++++------ bookwyrm/templates/login.html | 4 ++++ bookwyrm/templates/snippets/about.html | 4 ---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/bookwyrm/templates/about.html b/bookwyrm/templates/about.html index 4aae183a..25caf5e2 100644 --- a/bookwyrm/templates/about.html +++ b/bookwyrm/templates/about.html @@ -3,16 +3,14 @@
    -
    - {% include 'snippets/about.html' with site_settings=site_settings %} -
    + {% include 'snippets/about.html' with site_settings=site_settings %}

    Code of Conduct

    -
    - {{ site_settings.code_of_conduct }} -
    +
    + {{ site_settings.code_of_conduct | safe }} +
    {% endblock %} diff --git a/bookwyrm/templates/login.html b/bookwyrm/templates/login.html index 4e50c444..7ef88df3 100644 --- a/bookwyrm/templates/login.html +++ b/bookwyrm/templates/login.html @@ -51,6 +51,10 @@
    {% include 'snippets/about.html' with site_settings=site_settings %} + +

    + More about this site +

    diff --git a/bookwyrm/templates/snippets/about.html b/bookwyrm/templates/snippets/about.html index 92698206..596d77d5 100644 --- a/bookwyrm/templates/snippets/about.html +++ b/bookwyrm/templates/snippets/about.html @@ -5,7 +5,3 @@

    {{ site_settings.instance_description }}

    - -

    - More about this site -

    From 7bf39d3bf749f89c14fb0ab6930915629e1dc572 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 5 Nov 2020 16:48:15 -0800 Subject: [PATCH 140/416] html for updated reading progress flow --- bookwyrm/static/css/format.css | 4 + .../templates/snippets/create_status.html | 9 +- bookwyrm/templates/snippets/reply_form.html | 9 +- .../templates/snippets/shelve_button.html | 153 +++++++++++++++--- bookwyrm/templatetags/fr_display.py | 27 ++-- bookwyrm/views.py | 8 +- 6 files changed, 155 insertions(+), 55 deletions(-) diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/format.css index 51c931e6..db3c20ef 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/format.css @@ -15,6 +15,10 @@ input.toggle-control:checked ~ .toggle-content { display: block; } +input.toggle-control:checked ~ .modal.toggle-content { + display: flex; +} + /* --- STARS --- */ .rate-stars button.icon { background: none; diff --git a/bookwyrm/templates/snippets/create_status.html b/bookwyrm/templates/snippets/create_status.html index 28379f0e..9055e2d4 100644 --- a/bookwyrm/templates/snippets/create_status.html +++ b/bookwyrm/templates/snippets/create_status.html @@ -44,14 +44,7 @@
    -
    - -
    + {% include 'snippets/privacy_select.html' %}
    diff --git a/bookwyrm/templates/snippets/reply_form.html b/bookwyrm/templates/snippets/reply_form.html index 48371f63..2d8abd23 100644 --- a/bookwyrm/templates/snippets/reply_form.html +++ b/bookwyrm/templates/snippets/reply_form.html @@ -13,14 +13,7 @@
    -
    - -
    + {% include 'snippets/privacy_select.html' %}
    - - +
    + +
    + +
    + + +
    +{% endwith %} {% endif %} diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py index 73df1db8..5e719695 100644 --- a/bookwyrm/templatetags/fr_display.py +++ b/bookwyrm/templatetags/fr_display.py @@ -158,22 +158,14 @@ def time_since(date): @register.simple_tag(takes_context=True) -def shelve_button_identifier(context, book): +def active_shelf(context, book): ''' check what shelf a user has a book on, if any ''' #TODO: books can be on multiple shelves, handle that better shelf = models.ShelfBook.objects.filter( shelf__user=context['request'].user, book=book ).first() - if not shelf: - return 'to-read' - - identifier = shelf.shelf.identifier - if identifier == 'to-read': - return 'reading' - if identifier == 'reading': - return 'read' - return 'to-read' + return shelf.shelf if shelf else None @register.simple_tag(takes_context=True) @@ -192,7 +184,7 @@ def shelve_button_text(context, book): return 'Start reading' if identifier == 'reading': return 'I\'m done!' - return 'Want to read' + return 'Read' @register.simple_tag(takes_context=False) @@ -200,4 +192,15 @@ def latest_read_through(book, user): ''' the most recent read activity ''' return models.ReadThrough.objects.filter( user=user, - book=book).order_by('-created_date').first() + 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() diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 6e98225c..9794d585 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -71,7 +71,7 @@ def home_tab(request, tab): models.Edition.objects.filter( shelves__user=request.user, shelves__identifier='read' - )[:2], + ).order_by('-updated_date')[:2], # to-read models.Edition.objects.filter( shelves__user=request.user, @@ -242,7 +242,11 @@ def about_page(request): def password_reset_request(request): ''' invite management page ''' - return TemplateResponse(request, 'password_reset_request.html', {'title': 'Reset Password'}) + return TemplateResponse( + request, + 'password_reset_request.html', + {'title': 'Reset Password'} + ) def password_reset(request, code): From c64acf559b7f9f57fa6f8161eb072758c8fc40ac Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 08:51:50 -0800 Subject: [PATCH 141/416] create readthroughs --- bookwyrm/outgoing.py | 50 ++--- bookwyrm/status.py | 3 +- .../templates/snippets/shelve_button.html | 76 ++++---- bookwyrm/urls.py | 2 + bookwyrm/view_actions.py | 180 +++++++++++++----- 5 files changed, 200 insertions(+), 111 deletions(-) diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 444b2e71..0ea10ab9 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -1,5 +1,4 @@ ''' handles all the activity coming out of the server ''' -from datetime import datetime import re from django.db import IntegrityError, transaction @@ -121,6 +120,19 @@ def handle_shelve(user, book, shelf): broadcast(user, shelve.to_add_activity(user)) + +def handle_unshelve(user, book, shelf): + ''' a local user is getting a book put on their shelf ''' + # update the database + row = models.ShelfBook.objects.get(book=book, shelf=shelf) + activity = row.to_remove_activity(user) + row.delete() + + broadcast(user, activity) + + +def handle_reading_status(user, shelf, book, privacy): + ''' post about a user reading a book ''' # tell the world about this cool thing that happened try: message = { @@ -132,41 +144,17 @@ def handle_shelve(user, book, shelf): # it's a non-standard shelf, don't worry about it return - status = create_generated_note(user, message, mention_books=[book]) + status = create_generated_note( + user, + message, + mention_books=[book], + privacy=privacy + ) status.save() - if shelf.identifier == 'reading': - read = models.ReadThrough( - user=user, - book=book, - start_date=datetime.now()) - read.save() - elif shelf.identifier == 'read': - read = models.ReadThrough.objects.filter( - user=user, - book=book, - finish_date=None).order_by('-created_date').first() - if not read: - read = models.ReadThrough( - user=user, - book=book, - start_date=datetime.now()) - read.finish_date = datetime.now() - read.save() - broadcast(user, status.to_create_activity(user)) -def handle_unshelve(user, book, shelf): - ''' a local user is getting a book put on their shelf ''' - # update the database - row = models.ShelfBook.objects.get(book=book, shelf=shelf) - activity = row.to_remove_activity(user) - row.delete() - - broadcast(user, activity) - - def handle_imported_book(user, item, include_reviews, privacy): ''' process a goodreads csv and then post about it ''' if isinstance(item.book, models.Work): diff --git a/bookwyrm/status.py b/bookwyrm/status.py index b373bec5..c8c01c99 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -14,7 +14,7 @@ def delete_status(status): status.save() -def create_generated_note(user, content, mention_books=None): +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() @@ -24,6 +24,7 @@ def create_generated_note(user, content, mention_books=None): status = models.GeneratedNote.objects.create( user=user, content=content, + privacy=privacy ) if mention_books: diff --git a/bookwyrm/templates/snippets/shelve_button.html b/bookwyrm/templates/snippets/shelve_button.html index 5cb7d0d3..ab83ae73 100644 --- a/bookwyrm/templates/snippets/shelve_button.html +++ b/bookwyrm/templates/snippets/shelve_button.html @@ -30,15 +30,23 @@
    @@ -55,14 +63,14 @@
    @@ -74,7 +82,7 @@
    - +
    @@ -97,21 +105,21 @@ {% active_read_through book user as readthrough %}
    @@ -123,7 +131,7 @@
    - +
    diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 48c7d405..48095850 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -121,6 +121,8 @@ urlpatterns = [ re_path(r'^shelve/?$', actions.shelve), re_path(r'^unshelve/?$', actions.unshelve), + re_path(r'^start-reading/?$', actions.start_reading), + re_path(r'^finish-reading/?$', actions.finish_reading), re_path(r'^follow/?$', actions.follow), re_path(r'^unfollow/?$', actions.unfollow), diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index d6582ebc..c35a2dff 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -272,51 +272,6 @@ def upload_cover(request, book_id): return redirect('/book/%s' % book.id) -@login_required -def edit_readthrough(request): - ''' can't use the form because the dates are too finnicky ''' - try: - readthrough = models.ReadThrough.objects.get(id=request.POST.get('id')) - except models.ReadThrough.DoesNotExist: - return HttpResponseNotFound() - - # don't let people edit other people's data - if request.user != readthrough.user: - return HttpResponseBadRequest() - - # convert dates into a legible format - start_date = request.POST.get('start_date') - try: - start_date = dateutil.parser.parse(start_date) - except ParserError: - start_date = None - readthrough.start_date = start_date - finish_date = request.POST.get('finish_date') - try: - finish_date = dateutil.parser.parse(finish_date) - except ParserError: - finish_date = None - readthrough.finish_date = finish_date - readthrough.save() - return redirect(request.headers.get('Referer', '/')) - - -@login_required -def delete_readthrough(request): - ''' remove a readthrough ''' - try: - readthrough = models.ReadThrough.objects.get(id=request.POST.get('id')) - except models.ReadThrough.DoesNotExist: - return HttpResponseNotFound() - - # don't let people edit other people's data - if request.user != readthrough.user: - return HttpResponseBadRequest() - - readthrough.delete() - return redirect(request.headers.get('Referer', '/')) - - @login_required def shelve(request): ''' put a on a user's shelf ''' @@ -351,6 +306,107 @@ def unshelve(request): return redirect(request.headers.get('Referer', '/')) +@login_required +def start_reading(request): + ''' begin reading a book ''' + book = books_manager.get_edition(request.POST['book']) + shelf = models.Shelf.objects.filter( + identifier='reading', + user=request.user + ).first() + + # create a readthrough + readthrough = update_readthrough(request, book=book) + if readthrough.start_date: + readthrough.save() + + # shelve the book + if request.POST.get('reshelve', True): + try: + current_shelf = models.Shelf.objects.get( + user=request.user, + edition=book + ) + outgoing.handle_unshelve(request.user, book, current_shelf) + except models.Shelf.DoesNotExist: + # this just means it isn't currently on the user's shelves + pass + outgoing.handle_shelve(request.user, book, shelf) + + # post about it (if you want) + if request.POST.get('post-status'): + privacy = request.POST.get('privacy') + outgoing.handle_reading_status(request.user, shelf, book, privacy) + + return redirect(request.headers.get('Referer', '/')) + + +@login_required +def finish_reading(request): + ''' a user completed a book, yay ''' + book = books_manager.get_edition(request.POST['book']) + shelf = models.Shelf.objects.filter( + identifier='read', + user=request.user + ).first() + + # update or create a readthrough + readthrough = update_readthrough(request, book=book) + if readthrough.start_date or readthrough.finish_date: + readthrough.save() + + # shelve the book + if request.POST.get('reshelve', True): + try: + current_shelf = models.Shelf.objects.get( + user=request.user, + edition=book + ) + outgoing.handle_unshelve(request.user, book, current_shelf) + except models.Shelf.DoesNotExist: + # this just means it isn't currently on the user's shelves + pass + outgoing.handle_shelve(request.user, book, shelf) + + # post about it (if you want) + if request.POST.get('post-status'): + privacy = request.POST.get('privacy') + outgoing.handle_reading_status(request.user, shelf, book, privacy) + + return redirect(request.headers.get('Referer', '/')) + + +@login_required +def edit_readthrough(request): + ''' can't use the form because the dates are too finnicky ''' + readthrough = update_readthrough(request, create=False) + if not readthrough: + return HttpResponseNotFound() + + # don't let people edit other people's data + if request.user != readthrough.user: + return HttpResponseBadRequest() + readthrough.save() + + return redirect(request.headers.get('Referer', '/')) + + +@login_required +def delete_readthrough(request): + ''' remove a readthrough ''' + try: + readthrough = models.ReadThrough.objects.get(id=request.POST.get('id')) + except models.ReadThrough.DoesNotExist: + return HttpResponseNotFound() + + # don't let people edit other people's data + if request.user != readthrough.user: + return HttpResponseBadRequest() + + readthrough.delete() + return redirect(request.headers.get('Referer', '/')) + + @login_required def rate(request): ''' just a star rating for a book ''' @@ -578,3 +634,37 @@ def create_invite(request): invite.save() return redirect('/invite') + + +def update_readthrough(request, book=None, create=True): + ''' updates but does not save dates on a readthrough ''' + try: + read_id = request.POST.get('id') + if not read_id: + raise models.ReadThrough.DoesNotExist + readthrough = models.ReadThrough.objects.get(id=read_id) + except models.ReadThrough.DoesNotExist: + if not create or not book: + return None + readthrough = models.ReadThrough( + user=request.user, + book=book, + ) + + start_date = request.POST.get('start_date') + if start_date: + try: + start_date = dateutil.parser.parse(start_date) + readthrough.start_date = start_date + except ParserError: + pass + + finish_date = request.POST.get('finish_date') + if finish_date: + try: + finish_date = dateutil.parser.parse(finish_date) + readthrough.finish_date = finish_date + except ParserError: + pass + + return readthrough From 86f170b11cf644b6c30120d4034964621afc5cb9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 08:54:59 -0800 Subject: [PATCH 142/416] Functional cancel buttons --- bookwyrm/templates/snippets/shelve_button.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/snippets/shelve_button.html b/bookwyrm/templates/snippets/shelve_button.html index ab83ae73..0091bb4f 100644 --- a/bookwyrm/templates/snippets/shelve_button.html +++ b/bookwyrm/templates/snippets/shelve_button.html @@ -83,7 +83,7 @@
    - +
    @@ -132,7 +132,7 @@
    - +
    From 8f5d6c11ef6dd6e483a22ea36aacc9fb3864750a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 09:00:33 -0800 Subject: [PATCH 143/416] button spacing in shelve button pulldown --- bookwyrm/templates/snippets/shelve_button.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/snippets/shelve_button.html b/bookwyrm/templates/snippets/shelve_button.html index 0091bb4f..2688d06b 100644 --- a/bookwyrm/templates/snippets/shelve_button.html +++ b/bookwyrm/templates/snippets/shelve_button.html @@ -33,13 +33,13 @@ {% for shelf in request.user.shelf_set.all %}
  • {% if shelf.identifier == 'reading' and active_shelf.identifier != 'reading' %} -
  • From 681ebd136ab66518b1b0d31c5e5babf1834bf899 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 12:00:00 -0800 Subject: [PATCH 148/416] Links on user page --- bookwyrm/templates/user_header.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/user_header.html b/bookwyrm/templates/user_header.html index ecd1ec2d..bb892757 100644 --- a/bookwyrm/templates/user_header.html +++ b/bookwyrm/templates/user_header.html @@ -18,15 +18,18 @@ From 8550cbc710744eb11f9b2a9dc4599a9880d17f40 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 12:02:25 -0800 Subject: [PATCH 149/416] Move user header into snippets --- bookwyrm/templates/followers.html | 2 +- bookwyrm/templates/following.html | 2 +- bookwyrm/templates/shelf.html | 2 +- bookwyrm/templates/{ => snippets}/user_header.html | 0 bookwyrm/templates/user.html | 4 +--- bookwyrm/templates/user_shelves.html | 2 +- 6 files changed, 5 insertions(+), 7 deletions(-) rename bookwyrm/templates/{ => snippets}/user_header.html (100%) diff --git a/bookwyrm/templates/followers.html b/bookwyrm/templates/followers.html index 09457408..a5fdfd82 100644 --- a/bookwyrm/templates/followers.html +++ b/bookwyrm/templates/followers.html @@ -1,7 +1,7 @@ {% extends 'layout.html' %} {% load fr_display %} {% block content %} -{% include 'user_header.html' with user=user %} +{% include 'snippets/user_header.html' with user=user %}

    Followers

    diff --git a/bookwyrm/templates/following.html b/bookwyrm/templates/following.html index 9131adea..c3bf976a 100644 --- a/bookwyrm/templates/following.html +++ b/bookwyrm/templates/following.html @@ -1,7 +1,7 @@ {% extends 'layout.html' %} {% load fr_display %} {% block content %} -{% include 'user_header.html' %} +{% include 'snippets/user_header.html' with user=user %}

    Following

    diff --git a/bookwyrm/templates/shelf.html b/bookwyrm/templates/shelf.html index 96279543..8e6cc9f8 100644 --- a/bookwyrm/templates/shelf.html +++ b/bookwyrm/templates/shelf.html @@ -1,7 +1,7 @@ {% extends 'layout.html' %} {% load fr_display %} {% block content %} -{% include 'user_header.html' with user=user %} +{% include 'snippets/user_header.html' with user=user %}
    diff --git a/bookwyrm/templates/user_header.html b/bookwyrm/templates/snippets/user_header.html similarity index 100% rename from bookwyrm/templates/user_header.html rename to bookwyrm/templates/snippets/user_header.html diff --git a/bookwyrm/templates/user.html b/bookwyrm/templates/user.html index 3e409486..b6d808c8 100644 --- a/bookwyrm/templates/user.html +++ b/bookwyrm/templates/user.html @@ -1,9 +1,7 @@ {% extends 'layout.html' %} {% block content %} -
    - {% include 'user_header.html' with user=user %} -
    +{% include 'snippets/user_header.html' with user=user %}

    Shelves

    diff --git a/bookwyrm/templates/user_shelves.html b/bookwyrm/templates/user_shelves.html index af4f9d23..acda58ca 100644 --- a/bookwyrm/templates/user_shelves.html +++ b/bookwyrm/templates/user_shelves.html @@ -1,7 +1,7 @@ {% extends 'layout.html' %} {% load fr_display %} {% block content %} -{% include 'user_header.html' with user=user %} +{% include 'snippets/user_header.html' with user=user %}
    From c883893dd6e3ab4d415158df0eb1c500626970cb Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 12:09:14 -0800 Subject: [PATCH 150/416] Slightly less messy boost status header --- bookwyrm/templates/snippets/status.html | 26 +++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/bookwyrm/templates/snippets/status.html b/bookwyrm/templates/snippets/status.html index 9390d39c..45c12dce 100644 --- a/bookwyrm/templates/snippets/status.html +++ b/bookwyrm/templates/snippets/status.html @@ -5,18 +5,20 @@
    -

    - {% if status.status_type == 'Boost' %} - {% include 'snippets/avatar.html' with user=status.user %} - {% include 'snippets/username.html' with user=status.user %} - boosted -

    -

    - {% include 'snippets/status_header.html' with status=status|boosted_status %} - {% else %} - {% include 'snippets/status_header.html' with status=status %} - {% endif %} -

    +
    +
    + {% if status.status_type == 'Boost' %} + {% include 'snippets/avatar.html' with user=status.user %} + {% include 'snippets/username.html' with user=status.user %} + boosted +
    +
    + {% include 'snippets/status_header.html' with status=status|boosted_status %} + {% else %} + {% include 'snippets/status_header.html' with status=status %} + {% endif %} +
    +
    From f868471460c6c3ee800338865d1c468464948257 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 12:27:52 -0800 Subject: [PATCH 151/416] cleans up cover upload form --- bookwyrm/templates/book.html | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index 063116c1..adcccb9e 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -27,11 +27,18 @@ {% include 'snippets/shelve_button.html' %} {% if request.user.is_authenticated and not book.cover %} -
    - {% csrf_token %} - {{ cover_form.as_p }} - - +
    +
    + {% csrf_token %} +
    + + +
    +
    + +
    + +
    {% endif %}
    From 981628260aeb62726ad85cf9ebb210e3ac587a52 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 12:40:21 -0800 Subject: [PATCH 152/416] Don't show read dates of "None" --- bookwyrm/templates/book.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index adcccb9e..de286d62 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -67,10 +67,14 @@
    -
    +
    + +
    + + +
    {% endfor %} {% if request.user.is_authenticated %} diff --git a/bookwyrm/templates/snippets/shelve_button.html b/bookwyrm/templates/snippets/shelve_button.html index bf1db9b6..eee89728 100644 --- a/bookwyrm/templates/snippets/shelve_button.html +++ b/bookwyrm/templates/snippets/shelve_button.html @@ -87,14 +87,14 @@ {% include 'snippets/privacy_select.html' %}
    - +
    - +
    From 9ef63fff4a4a98571bdbfdd52fd9f1947daca281 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 13:03:54 -0800 Subject: [PATCH 154/416] More formatting for editing readthrough --- bookwyrm/templates/book.html | 44 ++++++++++--------- .../templates/snippets/shelve_button.html | 4 +- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index 9a0ad1e4..af79f57f 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -79,41 +79,43 @@
    -
    +
    diff --git a/bookwyrm/templates/snippets/shelve_button.html b/bookwyrm/templates/snippets/shelve_button.html index eee89728..84c560d7 100644 --- a/bookwyrm/templates/snippets/shelve_button.html +++ b/bookwyrm/templates/snippets/shelve_button.html @@ -63,7 +63,7 @@ {% endblock %} diff --git a/bookwyrm/templates/manage_invites.html b/bookwyrm/templates/manage_invites.html index b9bb1c00..01cc1ea9 100644 --- a/bookwyrm/templates/manage_invites.html +++ b/bookwyrm/templates/manage_invites.html @@ -29,21 +29,18 @@
    {% csrf_token %} -
    +
    +
    + {{ form.expiry }} +
    -
    - {{ form.expiry }} -
    -
    - -
    -
    -
    - {{ form.use_limit }} +
    + {{ form.use_limit }} +
    From a1242cd83fd4189ee99dcf0e27e6fdf78c3f4480 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 15:14:30 -0800 Subject: [PATCH 161/416] Invalid title for status page causing 500 --- bookwyrm/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 9794d585..454f5c08 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -430,7 +430,7 @@ def status_page(request, username, status_id): return JsonResponse(status.to_activity(), encoder=ActivityEncoder) data = { - 'title': status.type, + 'title': 'Status by %s' % user.username, 'status': status, } return TemplateResponse(request, 'status.html', data) From a8b1c1ce98ac4b161e673799e93f17177e790ed2 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 6 Nov 2020 15:20:11 -0800 Subject: [PATCH 162/416] button audit --- bookwyrm/templates/notifications.html | 1 - bookwyrm/templates/password_reset_request.html | 2 +- bookwyrm/templates/search_results.html | 2 +- bookwyrm/templates/snippets/follow_button.html | 6 +++--- bookwyrm/templates/snippets/follow_request_buttons.html | 4 ++-- bookwyrm/templates/snippets/shelf.html | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index afd4d8ae..d4ca5f8c 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -59,4 +59,3 @@
    {% endblock %} - diff --git a/bookwyrm/templates/password_reset_request.html b/bookwyrm/templates/password_reset_request.html index f66d84a9..a6df63b9 100644 --- a/bookwyrm/templates/password_reset_request.html +++ b/bookwyrm/templates/password_reset_request.html @@ -17,7 +17,7 @@
    - +
    diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html index cb997298..add82a7d 100644 --- a/bookwyrm/templates/search_results.html +++ b/bookwyrm/templates/search_results.html @@ -49,7 +49,7 @@ {% csrf_token %}
    {% include 'snippets/search_result_text.html' with result=result link=False %}
    - + {% endfor %} diff --git a/bookwyrm/templates/snippets/follow_button.html b/bookwyrm/templates/snippets/follow_button.html index 1f914a6c..3fdcbab8 100644 --- a/bookwyrm/templates/snippets/follow_button.html +++ b/bookwyrm/templates/snippets/follow_button.html @@ -11,14 +11,14 @@ Follow request already sent. {% csrf_token %} {% if user.manually_approves_followers %} - + {% else %} - + {% endif %} {% csrf_token %} - + {% endif %} diff --git a/bookwyrm/templates/snippets/follow_request_buttons.html b/bookwyrm/templates/snippets/follow_request_buttons.html index 165887e0..b6296d3f 100644 --- a/bookwyrm/templates/snippets/follow_request_buttons.html +++ b/bookwyrm/templates/snippets/follow_request_buttons.html @@ -3,11 +3,11 @@
    {% csrf_token %} - +
    {% csrf_token %} - + {% endif %} diff --git a/bookwyrm/templates/snippets/shelf.html b/bookwyrm/templates/snippets/shelf.html index adee004e..a9238a45 100644 --- a/bookwyrm/templates/snippets/shelf.html +++ b/bookwyrm/templates/snippets/shelf.html @@ -71,7 +71,7 @@ {% csrf_token %} -
    {% endfor %} diff --git a/bookwyrm/templates/snippets/shelf_selector.html b/bookwyrm/templates/snippets/shelf_selector.html new file mode 100644 index 00000000..7f93762a --- /dev/null +++ b/bookwyrm/templates/snippets/shelf_selector.html @@ -0,0 +1,31 @@ + From 794aeb299cbdcb33b1b6f088f2d154efa976f8a0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 11:54:32 -0800 Subject: [PATCH 169/416] Max on books shown from shelves in suggestions bar --- bookwyrm/templates/feed.html | 4 +++- bookwyrm/views.py | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bookwyrm/templates/feed.html b/bookwyrm/templates/feed.html index 085937d7..bc533db4 100644 --- a/bookwyrm/templates/feed.html +++ b/bookwyrm/templates/feed.html @@ -14,7 +14,9 @@ {% if shelf.books %} {% with shelf_counter=forloop.counter %}
  • -

    {{ shelf.name }}

    +

    + {{ shelf.name }} +

      {% for book in shelf.books %} diff --git a/bookwyrm/views.py b/bookwyrm/views.py index de3bac6c..67946491 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -87,10 +87,13 @@ def home_tab(request, tab): def get_suggested_books(user, max_books=5): ''' helper to get a user's recent books ''' book_count = 0 - preset_shelves = ['reading', 'read', 'to-read'] + preset_shelves = [ + ('reading', max_books), ('read', 2), ('to-read', max_books) + ] suggested_books = [] - for preset in preset_shelves: - limit = max_books - book_count + for (preset, shelf_max) in preset_shelves: + 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( From 3ba02f8fbd22be610bcd3c1146538f109b28c1af Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 12:07:09 -0800 Subject: [PATCH 170/416] Only report local users in user count --- bookwyrm/wellknown.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/wellknown.py b/bookwyrm/wellknown.py index 9daf31ee..d670fb32 100644 --- a/bookwyrm/wellknown.py +++ b/bookwyrm/wellknown.py @@ -58,15 +58,17 @@ def nodeinfo(request): return HttpResponseNotFound() status_count = models.Status.objects.filter(user__local=True).count() - user_count = models.User.objects.count() + user_count = models.User.objects.filter(local=True).count() month_ago = datetime.now() - relativedelta(months=1) last_month_count = models.User.objects.filter( + local=True, last_active_date__gt=month_ago ).count() six_months_ago = datetime.now() - relativedelta(months=6) six_month_count = models.User.objects.filter( + local=True, last_active_date__gt=six_months_ago ).count() return JsonResponse({ From 20395ff2ec527dd6dacf7b7b8d4524a7b53ae93d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 16:11:12 -0800 Subject: [PATCH 171/416] Corrects serialization of Add activity --- bookwyrm/models/shelf.py | 2 +- bookwyrm/models/tag.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/models/shelf.py b/bookwyrm/models/shelf.py index 24f1fdce..cd82198c 100644 --- a/bookwyrm/models/shelf.py +++ b/bookwyrm/models/shelf.py @@ -49,7 +49,7 @@ class ShelfBook(BookWyrmModel): return activitypub.Add( id='%s#add' % self.remote_id, actor=user.remote_id, - object=self.book.local_id, + object=self.book.to_activity(), target=self.shelf.remote_id, ).serialize() diff --git a/bookwyrm/models/tag.py b/bookwyrm/models/tag.py index 510ad13b..cd98e2b1 100644 --- a/bookwyrm/models/tag.py +++ b/bookwyrm/models/tag.py @@ -35,7 +35,7 @@ class Tag(OrderedCollectionMixin, BookWyrmModel): return activitypub.Add( id='%s#add' % self.remote_id, actor=user.remote_id, - object=self.book.local_id, + object=self.book.to_activity(), target=self.remote_id, ).serialize() From cfa4cb015df82101ecbf996d0f2b099f9f5021af Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 17:48:50 -0800 Subject: [PATCH 172/416] corrects tests for latest code changes --- bookwyrm/activitypub/base_activity.py | 11 +++--- .../tests/connectors/test_self_connector.py | 6 ++- bookwyrm/tests/incoming/test_favorite.py | 18 ++------- bookwyrm/tests/incoming/test_update_user.py | 1 + bookwyrm/tests/models/test_book_model.py | 13 +------ bookwyrm/tests/outgoing/test_shelving.py | 38 ------------------- bookwyrm/tests/test_books_manager.py | 2 + bookwyrm/tests/test_signing.py | 6 +-- 8 files changed, 22 insertions(+), 73 deletions(-) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 5f16906b..ee4eefbd 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -76,11 +76,12 @@ class ActivityObject: if not isinstance(self, model.activity_serializer): raise TypeError('Wrong activity type for model') - # check for an existing instance - try: - return model.objects.get(remote_id=self.id) - except model.DoesNotExist: - pass + # check for an existing instance, if we're not updating a known obj + if not instance: + try: + return model.objects.get(remote_id=self.id) + except model.DoesNotExist: + pass model_fields = [m.name for m in model._meta.get_fields()] mapped_fields = {} diff --git a/bookwyrm/tests/connectors/test_self_connector.py b/bookwyrm/tests/connectors/test_self_connector.py index 3dd4ac12..b80ad202 100644 --- a/bookwyrm/tests/connectors/test_self_connector.py +++ b/bookwyrm/tests/connectors/test_self_connector.py @@ -64,8 +64,10 @@ class SelfConnector(TestCase): def test_search_default_filter(self): - self.edition.default = True - self.edition.save() + ''' it should get rid of duplicate editions for the same work ''' + self.work.default_edition = self.edition + self.work.save() + results = self.connector.search('Anonymous') self.assertEqual(len(results), 1) self.assertEqual(results[0].title, 'Edition of Example Work') diff --git a/bookwyrm/tests/incoming/test_favorite.py b/bookwyrm/tests/incoming/test_favorite.py index eeba9000..c528d38c 100644 --- a/bookwyrm/tests/incoming/test_favorite.py +++ b/bookwyrm/tests/incoming/test_favorite.py @@ -17,6 +17,7 @@ class Favorite(TestCase): self.local_user = models.User.objects.create_user( 'mouse', 'mouse@mouse.com', 'mouseword', remote_id='http://local.com/user/mouse') + self.status = models.Status.objects.create( user=self.local_user, content='Test status', @@ -33,24 +34,13 @@ class Favorite(TestCase): def test_handle_favorite(self): activity = { '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'http://example.com/activity/1', - - 'type': 'Create', + 'id': 'http://example.com/fav/1', 'actor': 'https://example.com/users/rat', 'published': 'Mon, 25 May 2020 19:31:20 GMT', - 'to': ['https://example.com/user/rat/followers'], - 'cc': ['https://www.w3.org/ns/activitystreams#Public'], - 'object': { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'http://example.com/fav/1', - 'type': 'Like', - 'actor': 'https://example.com/users/rat', - 'object': 'http://local.com/status/1', - }, - 'signature': {} + 'object': 'http://local.com/status/1', } - result = incoming.handle_favorite(activity) + incoming.handle_favorite(activity) fav = models.Favorite.objects.get(remote_id='http://example.com/fav/1') self.assertEqual(fav.status, self.status) diff --git a/bookwyrm/tests/incoming/test_update_user.py b/bookwyrm/tests/incoming/test_update_user.py index 703078f1..7ac038eb 100644 --- a/bookwyrm/tests/incoming/test_update_user.py +++ b/bookwyrm/tests/incoming/test_update_user.py @@ -1,3 +1,4 @@ +''' when a remote user changes their profile ''' import json import pathlib from django.test import TestCase diff --git a/bookwyrm/tests/models/test_book_model.py b/bookwyrm/tests/models/test_book_model.py index e8211a8f..7dfad61f 100644 --- a/bookwyrm/tests/models/test_book_model.py +++ b/bookwyrm/tests/models/test_book_model.py @@ -39,17 +39,8 @@ class Book(TestCase): title='Invalid Book' ) - def test_default_edition(self): - ''' a work should always be able to produce a deafult edition ''' - self.assertIsInstance(self.work.default_edition, models.Edition) - self.assertEqual(self.work.default_edition, self.first_edition) - - self.second_edition.default = True - self.second_edition.save() - - self.assertEqual(self.work.default_edition, self.second_edition) - def test_isbn_10_to_13(self): + ''' checksums and so on ''' isbn_10 = '178816167X' isbn_13 = isbn_10_to_13(isbn_10) self.assertEqual(isbn_13, '9781788161671') @@ -59,8 +50,8 @@ class Book(TestCase): self.assertEqual(isbn_13, '9781788161671') - def test_isbn_13_to_10(self): + ''' checksums and so on ''' isbn_13 = '9781788161671' isbn_10 = isbn_13_to_10(isbn_13) self.assertEqual(isbn_10, '178816167X') diff --git a/bookwyrm/tests/outgoing/test_shelving.py b/bookwyrm/tests/outgoing/test_shelving.py index cc2b6b17..0b85b671 100644 --- a/bookwyrm/tests/outgoing/test_shelving.py +++ b/bookwyrm/tests/outgoing/test_shelving.py @@ -38,16 +38,6 @@ class Shelving(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - # it should have posted a status about this - status = models.GeneratedNote.objects.get() - self.assertEqual(status.content, 'wants to read') - self.assertEqual(status.user, self.user) - self.assertEqual(status.mention_books.count(), 1) - self.assertEqual(status.mention_books.first(), self.book) - - # and it should not create a read-through - self.assertEqual(models.ReadThrough.objects.count(), 0) - def test_handle_shelve_reading(self): shelf = models.Shelf.objects.get(identifier='reading') @@ -56,20 +46,6 @@ class Shelving(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - # it should have posted a status about this - status = models.GeneratedNote.objects.order_by('-published_date').first() - self.assertEqual(status.content, 'started reading') - self.assertEqual(status.user, self.user) - self.assertEqual(status.mention_books.count(), 1) - self.assertEqual(status.mention_books.first(), self.book) - - # and it should create a read-through - readthrough = models.ReadThrough.objects.get() - self.assertEqual(readthrough.user, self.user) - self.assertEqual(readthrough.book.id, self.book.id) - self.assertIsNotNone(readthrough.start_date) - self.assertIsNone(readthrough.finish_date) - def test_handle_shelve_read(self): shelf = models.Shelf.objects.get(identifier='read') @@ -78,20 +54,6 @@ class Shelving(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - # it should have posted a status about this - status = models.GeneratedNote.objects.order_by('-published_date').first() - self.assertEqual(status.content, 'finished reading') - self.assertEqual(status.user, self.user) - self.assertEqual(status.mention_books.count(), 1) - self.assertEqual(status.mention_books.first(), self.book) - - # and it should update the existing read-through - readthrough = models.ReadThrough.objects.get() - self.assertEqual(readthrough.user, self.user) - self.assertEqual(readthrough.book.id, self.book.id) - self.assertIsNotNone(readthrough.start_date) - self.assertIsNotNone(readthrough.finish_date) - def test_handle_unshelve(self): self.shelf.books.add(self.book) diff --git a/bookwyrm/tests/test_books_manager.py b/bookwyrm/tests/test_books_manager.py index 46186838..039bdfc5 100644 --- a/bookwyrm/tests/test_books_manager.py +++ b/bookwyrm/tests/test_books_manager.py @@ -15,6 +15,8 @@ class Book(TestCase): 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', diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py index ed5600ac..8653823d 100644 --- a/bookwyrm/tests/test_signing.py +++ b/bookwyrm/tests/test_signing.py @@ -39,6 +39,7 @@ class Signature(TestCase): ) def send(self, signature, now, data, digest): + ''' test request ''' c = Client() return c.post( urlsplit(self.rat.inbox).path, @@ -73,13 +74,13 @@ class Signature(TestCase): def test_wrong_signature(self): ''' Messages must be signed by the right actor. - (cat cannot sign messages on behalf of mouse) - ''' + (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') data = json.loads(datafile.read_bytes()) data['id'] = self.fake_remote.remote_id @@ -138,7 +139,6 @@ class Signature(TestCase): json=data, status=200) - # Key correct: response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200) From e6d46878fb691d7d24b7ddeb7d0c9de53f9238d0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 18:18:44 -0800 Subject: [PATCH 173/416] Fixes like/unlike statuses --- bookwyrm/incoming.py | 7 ++++++- bookwyrm/outgoing.py | 3 ++- bookwyrm/templates/snippets/boost_button.html | 4 ++-- bookwyrm/templates/snippets/create_status_form.html | 2 +- bookwyrm/templates/snippets/fav_button.html | 4 ++-- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index a7823f43..ad44fd97 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -269,7 +269,12 @@ def handle_favorite(activity): @app.task def handle_unfavorite(activity): ''' approval of your good good post ''' - like = activitypub.Like(**activity['object']).to_model(models.Favorite) + try: + like = models.Favorite.objects.filter( + remote_id=activity['object']['id'] + ).first() + except models.Favorite.DoesNotExist: + return like.delete() diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 1b21603e..7a21048b 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -299,7 +299,8 @@ def handle_unfavorite(user, status): # can't find that status, idk return - fav_activity = activitypub.Undo(actor=user, object=favorite) + fav_activity = favorite.to_undo_activity(user) + favorite.delete() broadcast(user, fav_activity, direct_recipients=[status.user]) diff --git a/bookwyrm/templates/snippets/boost_button.html b/bookwyrm/templates/snippets/boost_button.html index 7133e818..2c747fe2 100644 --- a/bookwyrm/templates/snippets/boost_button.html +++ b/bookwyrm/templates/snippets/boost_button.html @@ -1,6 +1,6 @@ {% load fr_display %} {% with activity.id|uuid as uuid %} -
      + {% csrf_token %} -
      + {% csrf_token %}
    {% endif %} - + {% if type == 'quote' %} diff --git a/bookwyrm/templates/snippets/fav_button.html b/bookwyrm/templates/snippets/fav_button.html index de41064a..833a6199 100644 --- a/bookwyrm/templates/snippets/fav_button.html +++ b/bookwyrm/templates/snippets/fav_button.html @@ -1,6 +1,6 @@ {% load fr_display %} {% with activity.id|uuid as uuid %} - + {% csrf_token %} -
    + {% csrf_token %} -
    + {% csrf_token %} -
    + {% csrf_token %} - - - {% endif %} - - - - -{% else %} -
    -
    -

    + {% if status.status_type == 'Boost' %} {% include 'snippets/avatar.html' with user=status.user %} {% include 'snippets/username.html' with user=status.user %} - deleted this status -

    -
    -
    + boosted + {% include 'snippets/status_body.html' with status=status|boosted_status %} + {% else %} + {% include 'snippets/status_body.html' with status=status %} + {% endif %} {% endif %} diff --git a/bookwyrm/templates/snippets/status_body.html b/bookwyrm/templates/snippets/status_body.html new file mode 100644 index 00000000..9eec95cd --- /dev/null +++ b/bookwyrm/templates/snippets/status_body.html @@ -0,0 +1,120 @@ +{% load fr_display %} +{% load humanize %} + +{% if not status.deleted %} +
    +
    +
    +
    +
    + {% include 'snippets/status_header.html' with status=status %} +
    +
    +
    +
    + +
    + {% include 'snippets/status_content.html' with status=status %} +
    + +
    + {% if request.user.is_authenticated %} + + + {% endif %} + + +
    + + +
    +
    +
    +{% else %} +
    +
    +

    + {% include 'snippets/avatar.html' with user=status.user %} + {% include 'snippets/username.html' with user=status.user %} + deleted this status +

    +
    +
    +{% endif %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 147d992d..6399c87e 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -116,7 +116,7 @@ urlpatterns = [ re_path(r'^favorite/(?P\d+)/?$', actions.favorite), re_path(r'^unfavorite/(?P\d+)/?$', actions.unfavorite), re_path(r'^boost/(?P\d+)/?$', actions.boost), - re_path(r'^unboost/(?P\d+)/?$', actions.boost), + re_path(r'^unboost/(?P\d+)/?$', actions.unboost), re_path(r'^delete-status/?$', actions.delete_status), From 4710e65269e92d8b490a904d5ce1d1b1b3706c1a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 19:15:04 -0800 Subject: [PATCH 178/416] create notification for local favs/boosts --- bookwyrm/incoming.py | 9 ++++----- bookwyrm/outgoing.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index bcb021ca..8eca4cfb 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -269,11 +269,10 @@ def handle_favorite(activity): @app.task def handle_unfavorite(activity): ''' approval of your good good post ''' - try: - like = models.Favorite.objects.filter( - remote_id=activity['object']['id'] - ).first() - except models.Favorite.DoesNotExist: + like = models.Favorite.objects.filter( + remote_id=activity['object']['id'] + ).first() + if not like: return like.delete() diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index c0253a99..a196fcec 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -286,6 +286,12 @@ def handle_favorite(user, status): fav_activity = favorite.to_activity() broadcast( user, fav_activity, privacy='direct', direct_recipients=[status.user]) + create_notification( + status.user, + 'FAVORITE', + related_user=user, + related_status=status + ) def handle_unfavorite(user, status): @@ -319,6 +325,13 @@ def handle_boost(user, status): boost_activity = boost.to_activity() broadcast(user, boost_activity) + create_notification( + status.user, + 'BOOST', + related_user=user, + related_status=status + ) + def handle_unboost(user, status): ''' a user regrets boosting a status ''' From 50aaa8d9a1e98c25554fd1412702e4c73a9dc770 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 20:13:13 -0800 Subject: [PATCH 179/416] Don't error on statuses that didn't get created --- bookwyrm/incoming.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 8eca4cfb..e9c5f6a8 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -222,6 +222,8 @@ def handle_create(activity): return status = status_builder.create_status(activity['object']) + if not status: + return # create a notification if this is a reply if status.reply_parent and status.reply_parent.user.local: From e21d59386c7fd2dc739158ef7007c5edffd02c86 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 20:47:56 -0800 Subject: [PATCH 180/416] Don't show reshelve buttons on other people's shelves yikes --- bookwyrm/templates/snippets/shelf.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bookwyrm/templates/snippets/shelf.html b/bookwyrm/templates/snippets/shelf.html index 8629ed1a..1ca5ed60 100644 --- a/bookwyrm/templates/snippets/shelf.html +++ b/bookwyrm/templates/snippets/shelf.html @@ -66,9 +66,11 @@ {% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %} {% endif %} + {% if shelf.user == request.user %}
  • + {% endif %} {% endfor %}
    -
    - {% csrf_token %} - - - -
    + {% include 'snippets/shelf_selector.html' with current=shelf %}
    {% include 'snippets/shelf_selector.html' with current=shelf %}
    From 01f7d2ac44b7d9a301b3cf4d316619bd53cbcc05 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 21:07:07 -0800 Subject: [PATCH 181/416] Updates nodeinfo and api info --- bookwyrm/wellknown.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/bookwyrm/wellknown.py b/bookwyrm/wellknown.py index d670fb32..29cee707 100644 --- a/bookwyrm/wellknown.py +++ b/bookwyrm/wellknown.py @@ -71,6 +71,8 @@ def nodeinfo(request): local=True, last_active_date__gt=six_months_ago ).count() + + site = models.SiteSettings.get() return JsonResponse({ 'version': '2.0', 'software': { @@ -88,33 +90,34 @@ def nodeinfo(request): }, 'localPosts': status_count, }, - 'openRegistrations': True, + 'openRegistrations': site.allow_registration, }) def instance_info(request): - ''' what this place is TODO: should be settable/editable ''' + ''' let's talk about your cool unique instance ''' if request.method != 'GET': return HttpResponseNotFound() - user_count = models.User.objects.count() - status_count = models.Status.objects.count() + 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': 'BookWyrm', - 'short_description': 'Social reading, decentralized', - 'description': '', - 'email': 'mousereeve@riseup.net', + 'title': site.name, + 'short_description': '', + 'description': site.instance_description, 'version': '0.0.1', 'stats': { 'user_count': user_count, 'status_count': status_count, }, - 'thumbnail': '', # TODO: logo thumbnail + 'thumbnail': 'https://%s/static/images/logo.png' % DOMAIN, 'languages': [ 'en' ], - 'registrations': True, + 'registrations': site.allow_registration, 'approval_required': False, }) From da5af56f720c9f063c864427faedd30bf3ac74ae Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 7 Nov 2020 21:17:52 -0800 Subject: [PATCH 182/416] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..d35f90eb --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# ******** NOTE ******** + +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '18 6 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From a6073caba0e4423ba8bd7bef30a7b7bca46dc56e Mon Sep 17 00:00:00 2001 From: Emil Date: Sun, 8 Nov 2020 17:02:01 +0100 Subject: [PATCH 183/416] Fixed typo in readme shleves -> shelves --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71b1a493..4d67bd85 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Since the project is still in its early stages, not everything here is fully imp - Differentiate local and federated reviews and rating - Track reading activity - Shelve books on default "to-read," "currently reading," and "read" shelves - - Create custom shleves + - Create custom shelves - Store started reading/finished reading dates - Update followers about reading activity (optionally, and with granular privacy controls) - Federation with ActivityPub From edc653e273c54afebf8ebc8dccebb9372398a1a1 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 8 Nov 2020 09:45:42 -0800 Subject: [PATCH 184/416] Fixes typo in edit user manually approve followers label --- bookwyrm/templates/edit_user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/edit_user.html b/bookwyrm/templates/edit_user.html index a95cbca2..14d17024 100644 --- a/bookwyrm/templates/edit_user.html +++ b/bookwyrm/templates/edit_user.html @@ -37,7 +37,7 @@ {% endfor %}

    -

    - Notitications + Notifications {% if request.user|notification_count %} From 77b0a3b67f22f881f096ebe767cc53433f2ccc40 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 8 Nov 2020 12:25:05 -0800 Subject: [PATCH 195/416] Adds alt text to avatars --- bookwyrm/templates/snippets/avatar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/snippets/avatar.html b/bookwyrm/templates/snippets/avatar.html index ab621777..3d6e65cc 100644 --- a/bookwyrm/templates/snippets/avatar.html +++ b/bookwyrm/templates/snippets/avatar.html @@ -1,2 +1,2 @@ - +avatar for {{ user|username }} From 8f95c1e7284117705dbe10cfe65468cafe922dc4 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 8 Nov 2020 12:38:27 -0800 Subject: [PATCH 196/416] Clearer logo link to home page and missing filter import --- bookwyrm/templates/layout.html | 2 +- bookwyrm/templates/snippets/avatar.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index b1634ff1..6c3cd345 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -23,7 +23,7 @@