diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 852db345..85245929 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -2,11 +2,11 @@ import inspect import sys -from .base_activity import ActivityEncoder, Image, PublicKey, Signature +from .base_activity import ActivityEncoder, PublicKey, Signature from .base_activity import Link, Mention from .base_activity import ActivitySerializerError from .base_activity import tag_formatter -from .base_activity import image_formatter, image_attachments_formatter +from .image import Image from .note import Note, GeneratedNote, Article, Comment, Review, Quotation from .note import Tombstone from .interaction import Boost, Like diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 54c2baea..caa4aeb8 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -4,6 +4,7 @@ from json import JSONEncoder from uuid import uuid4 from django.core.files.base import ContentFile +from django.db import transaction from django.db.models.fields.related_descriptors \ import ForwardManyToOneDescriptor, ManyToManyDescriptor, \ ReverseManyToOneDescriptor @@ -23,13 +24,6 @@ class ActivityEncoder(JSONEncoder): return o.__dict__ -@dataclass -class Image: - ''' image block ''' - url: str - type: str = 'Image' - - @dataclass class Link(): ''' for tagging a book in a status ''' @@ -113,14 +107,15 @@ class ActivityObject: formatted_value = mapping.model_formatter(value) if isinstance(model_field, ForwardManyToOneDescriptor) and \ formatted_value: - # foreign key remote id reolver + # foreign key remote id reolver (work on Edition, for example) fk_model = model_field.field.related_model reference = resolve_foreign_key(fk_model, formatted_value) mapped_fields[mapping.model_key] = reference elif isinstance(model_field, ManyToManyDescriptor): + # status mentions book/users many_to_many_fields[mapping.model_key] = formatted_value elif isinstance(model_field, ReverseManyToOneDescriptor): - # attachments on statuses, for example + # attachments on Status, for example one_to_many_fields[mapping.model_key] = formatted_value elif isinstance(model_field, ImageFileDescriptor): # image fields need custom handling @@ -128,37 +123,41 @@ class ActivityObject: else: mapped_fields[mapping.model_key] = formatted_value - if instance: - # updating an existing model isntance - for k, v in mapped_fields.items(): - setattr(instance, k, v) - instance.save() - else: - # creating a new model instance - instance = model.objects.create(**mapped_fields) - - # add many-to-many fields - for (model_key, values) in many_to_many_fields.items(): - getattr(instance, model_key).set(values) - instance.save() - - # add images - for (model_key, value) in image_fields.items(): - if not value: - continue - getattr(instance, model_key).save(*value, save=True) - - # add one to many fields - for (model_key, values) in one_to_many_fields.items(): - items = [] - for item in values: - # the reference id wasn't available at creation time - setattr(item, instance.__class__.__name__.lower(), instance) - item.save() - items.append(item) - if items: - getattr(instance, model_key).set(items) + with transaction.atomic(): + if instance: + # updating an existing model isntance + for k, v in mapped_fields.items(): + setattr(instance, k, v) instance.save() + else: + # creating a new model instance + instance = model.objects.create(**mapped_fields) + + # add images + for (model_key, value) in image_fields.items(): + formatted_value = image_formatter(value) + if not formatted_value: + continue + getattr(instance, model_key).save(*formatted_value, save=True) + + for (model_key, values) in many_to_many_fields.items(): + # mention books, mention users + getattr(instance, model_key).set(values) + + # add one to many fields + for (model_key, values) in one_to_many_fields.items(): + if values == MISSING: + continue + model_field = getattr(instance, model_key) + model = model_field.model + for item in values: + item = model.activity_serializer(**item) + field_name = instance.__class__.__name__.lower() + with transaction.atomic(): + item = item.to_model(model) + setattr(item, field_name, instance) + item.save() + return instance @@ -210,18 +209,18 @@ def tag_formatter(tags, tag_type): return items -def image_formatter(image_json): +def image_formatter(image_slug): ''' helper function to load images and format them for a model ''' - if isinstance(image_json, list): - try: - image_json = image_json[0] - except IndexError: - return None - - if not image_json or not hasattr(image_json, 'url'): + # when it's an inline image (User avatar/icon, Book cover), it's a json + # blob, but when it's an attached image, it's just a url + if isinstance(image_slug, dict): + url = image_slug.get('url') + elif isinstance(image_slug, str): + url = image_slug + else: + return None + if not url: return None - url = image_json.get('url') - try: response = requests.get(url) except ConnectionError: @@ -232,17 +231,3 @@ def image_formatter(image_json): image_name = str(uuid4()) + '.' + url.split('.')[-1] image_content = ContentFile(response.content) return [image_name, image_content] - - -def image_attachments_formatter(images_json): - ''' deserialize a list of images ''' - attachments = [] - for image in images_json: - caption = image.get('name') - attachment = models.Attachment(caption=caption) - image_field = image_formatter(image) - if not image_field: - continue - attachment.image.save(*image_field, save=False) - attachments.append(attachment) - return attachments diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 2bfafba7..02cab281 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -2,42 +2,43 @@ from dataclasses import dataclass, field from typing import List -from .base_activity import ActivityObject, Image +from .base_activity import ActivityObject +from .image import Image @dataclass(init=False) class Book(ActivityObject): ''' serializes an edition or work, abstract ''' - authors: List[str] - first_published_date: str - published_date: str - title: str - sort_title: str - subtitle: str - description: str + sortTitle: str = '' + subtitle: str = '' + description: str = '' languages: List[str] - series: str - series_number: str + series: str = '' + seriesNumber: str = '' subjects: List[str] - subject_places: List[str] + subjectPlaces: List[str] - openlibrary_key: str - librarything_key: str - goodreads_key: str + authors: List[str] + firstPublishedDate: str = '' + publishedDate: str = '' - attachment: List[Image] = field(default_factory=lambda: []) + openlibraryKey: str = '' + librarythingKey: str = '' + goodreadsKey: str = '' + + cover: Image = field(default_factory=lambda: {}) type: str = 'Book' @dataclass(init=False) class Edition(Book): ''' Edition instance of a book object ''' - isbn_10: str - isbn_13: str - oclc_number: str + isbn10: str + isbn13: str + oclcNumber: str asin: str pages: str - physical_format: str + physicalFormat: str publishers: List[str] work: str diff --git a/bookwyrm/activitypub/image.py b/bookwyrm/activitypub/image.py new file mode 100644 index 00000000..569f83c5 --- /dev/null +++ b/bookwyrm/activitypub/image.py @@ -0,0 +1,11 @@ +''' an image, nothing fancy ''' +from dataclasses import dataclass +from .base_activity import ActivityObject + +@dataclass(init=False) +class Image(ActivityObject): + ''' image block ''' + url: str + name: str = '' + type: str = 'Image' + id: str = '' diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 9eab952d..aeb078dc 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -2,7 +2,8 @@ from dataclasses import dataclass, field from typing import Dict, List -from .base_activity import ActivityObject, Image, Link +from .base_activity import ActivityObject, Link +from .image import Image @dataclass(init=False) class Tombstone(ActivityObject): diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index 324d68e3..e7d720ec 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -2,7 +2,8 @@ from dataclasses import dataclass, field from typing import Dict -from .base_activity import ActivityObject, Image, PublicKey +from .base_activity import ActivityObject, PublicKey +from .image import Image @dataclass(init=False) class Person(ActivityObject): diff --git a/bookwyrm/migrations/0014_auto_20201128_0118.py b/bookwyrm/migrations/0014_auto_20201128_0118.py new file mode 100644 index 00000000..babdd780 --- /dev/null +++ b/bookwyrm/migrations/0014_auto_20201128_0118.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.7 on 2020-11-28 01:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0013_book_origin_id'), + ] + + operations = [ + migrations.RenameModel( + old_name='Attachment', + new_name='Image', + ), + ] diff --git a/bookwyrm/migrations/0015_auto_20201128_0349.py b/bookwyrm/migrations/0015_auto_20201128_0349.py new file mode 100644 index 00000000..52b15518 --- /dev/null +++ b/bookwyrm/migrations/0015_auto_20201128_0349.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-11-28 03:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0014_auto_20201128_0118'), + ] + + operations = [ + migrations.AlterField( + model_name='image', + name='status', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status'), + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 81c64831..3d854478 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -5,15 +5,21 @@ import sys from .book import Book, Work, Edition from .author import Author from .connector import Connector -from .relationship import UserFollows, UserFollowRequest, UserBlocks + from .shelf import Shelf, ShelfBook + from .status import Status, GeneratedNote, Review, Comment, Quotation -from .status import Attachment, Favorite, Boost, Notification, ReadThrough +from .status import Favorite, Boost, Notification, ReadThrough +from .attachment import Image + from .tag import Tag + from .user import User +from .relationship import UserFollows, UserFollowRequest, UserBlocks from .federated_server import FederatedServer from .import_job import ImportJob, ImportItem + from .site import SiteSettings, SiteInvite, PasswordReset cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) diff --git a/bookwyrm/models/attachment.py b/bookwyrm/models/attachment.py new file mode 100644 index 00000000..7329e65d --- /dev/null +++ b/bookwyrm/models/attachment.py @@ -0,0 +1,32 @@ +''' media that is posted in the app ''' +from django.db import models + +from bookwyrm import activitypub +from .base_model import ActivitypubMixin +from .base_model import ActivityMapping, BookWyrmModel + + +class Attachment(ActivitypubMixin, BookWyrmModel): + ''' an image (or, in the future, video etc) associated with a status ''' + status = models.ForeignKey( + 'Status', + on_delete=models.CASCADE, + related_name='attachments', + null=True + ) + class Meta: + ''' one day we'll have other types of attachments besides images ''' + abstract = True + + activity_mappings = [ + ActivityMapping('id', 'remote_id'), + ActivityMapping('url', 'image'), + ActivityMapping('name', 'caption'), + ] + +class Image(Attachment): + ''' an image attachment ''' + image = models.ImageField(upload_to='status/', null=True, blank=True) + caption = models.TextField(null=True, blank=True) + + activity_serializer = activitypub.Image diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 8c28c8ab..4109a49b 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -10,6 +10,7 @@ from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256 from django.db import models +from django.db.models.fields.files import ImageFieldFile from django.dispatch import receiver from bookwyrm import activitypub @@ -77,16 +78,18 @@ class ActivitypubMixin: value = value.remote_id elif isinstance(value, datetime): value = value.isoformat() + elif isinstance(value, ImageFieldFile): + value = image_formatter(value) # run the custom formatter function set in the model - result = mapping.activity_formatter(value) + formatted_value = mapping.activity_formatter(value) if mapping.activity_key in fields and \ isinstance(fields[mapping.activity_key], list): # there can be two database fields that map to the same AP list # this happens in status tags, which combines user and book tags - fields[mapping.activity_key] += result + fields[mapping.activity_key] += formatted_value else: - fields[mapping.activity_key] = result + fields[mapping.activity_key] = formatted_value if pure: return self.pure_activity_serializer( @@ -270,12 +273,10 @@ def tag_formatter(items, name_field, activity_type): return tags -def image_formatter(image, default_path=None): +def image_formatter(image): ''' convert images into activitypub json ''' if image and hasattr(image, 'url'): url = image.url - elif default_path: - url = default_path else: return None url = 'https://%s%s' % (DOMAIN, url) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 7ec330da..132b4c07 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -12,7 +12,6 @@ from bookwyrm.utils.fields import ArrayField from .base_model import ActivityMapping, BookWyrmModel from .base_model import ActivitypubMixin, OrderedCollectionPageMixin -from .base_model import image_attachments_formatter class Book(ActivitypubMixin, BookWyrmModel): ''' a generic book, which can mean either an edition or a work ''' @@ -61,49 +60,39 @@ 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_parent_work(self): - ''' reference the work via local id not remote ''' - return self.parent_work.remote_id - activity_mappings = [ ActivityMapping('id', 'remote_id'), ActivityMapping('authors', 'ap_authors'), - ActivityMapping('first_published_date', 'first_published_date'), - ActivityMapping('published_date', 'published_date'), + ActivityMapping('firstPublishedDate', 'firstpublished_date'), + ActivityMapping('publishedDate', 'published_date'), ActivityMapping('title', 'title'), - ActivityMapping('sort_title', 'sort_title'), + ActivityMapping('sortTitle', 'sort_title'), ActivityMapping('subtitle', 'subtitle'), ActivityMapping('description', 'description'), ActivityMapping('languages', 'languages'), ActivityMapping('series', 'series'), - ActivityMapping('series_number', 'series_number'), + ActivityMapping('seriesNumber', 'series_number'), ActivityMapping('subjects', 'subjects'), - ActivityMapping('subject_places', 'subject_places'), + ActivityMapping('subjectPlaces', 'subject_places'), - ActivityMapping('openlibrary_key', 'openlibrary_key'), - ActivityMapping('librarything_key', 'librarything_key'), - ActivityMapping('goodreads_key', 'goodreads_key'), + ActivityMapping('openlibraryKey', 'openlibrary_key'), + ActivityMapping('librarythingKey', 'librarything_key'), + ActivityMapping('goodreadsKey', 'goodreads_key'), - ActivityMapping('work', 'ap_parent_work'), - ActivityMapping('isbn_10', 'isbn_10'), - ActivityMapping('isbn_13', 'isbn_13'), - ActivityMapping('oclc_number', 'oclc_number'), + ActivityMapping('work', 'parent_work'), + ActivityMapping('isbn10', 'isbn_10'), + ActivityMapping('isbn13', 'isbn_13'), + ActivityMapping('oclcNumber', 'oclc_number'), ActivityMapping('asin', 'asin'), ActivityMapping('pages', 'pages'), - ActivityMapping('physical_format', 'physical_format'), + ActivityMapping('physicalFormat', 'physical_format'), ActivityMapping('publishers', 'publishers'), ActivityMapping('lccn', 'lccn'), ActivityMapping('editions', 'editions_path'), - ActivityMapping( - 'attachment', 'cover', - # this expects an iterable and the field is just an image - lambda x: image_attachments_formatter([x]), - activitypub.image_formatter - ), + ActivityMapping('cover', 'cover'), ] def save(self, *args, **kwargs): diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 6f534f50..9d45379c 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -90,7 +90,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ActivityMapping( 'attachment', 'attachments', lambda x: image_attachments_formatter(x.all()), - activitypub.image_attachments_formatter ) ] @@ -151,17 +150,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): return super().save(*args, **kwargs) -class Attachment(BookWyrmModel): - ''' an image (or, in the future, video etc) associated with a status ''' - status = models.ForeignKey( - 'Status', - on_delete=models.CASCADE, - related_name='attachments' - ) - image = models.ImageField(upload_to='status/', null=True, blank=True) - caption = models.TextField(null=True, blank=True) - - class GeneratedNote(Status): ''' these are app-generated messages about user activity ''' @property diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index b38a4b19..4d511d56 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -112,11 +112,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): activity_formatter=lambda x: {'sharedInbox': x}, model_formatter=lambda x: x.get('sharedInbox') ), - ActivityMapping( - 'icon', 'avatar', - lambda x: image_formatter(x, '/static/images/default_avi.jpg'), - activitypub.image_formatter - ), + ActivityMapping('icon', 'avatar'), ActivityMapping( 'manuallyApprovesFollowers', 'manually_approves_followers' diff --git a/bookwyrm/tests/data/ap_quotation.json b/bookwyrm/tests/data/ap_quotation.json index 089bc85f..36a4112b 100644 --- a/bookwyrm/tests/data/ap_quotation.json +++ b/bookwyrm/tests/data/ap_quotation.json @@ -13,14 +13,6 @@ "sensitive": false, "content": "commentary", "type": "Quotation", - "attachment": [ - { - "type": "Document", - "mediaType": "image//images/covers/2b4e4712-5a4d-4ac1-9df4-634cc9c7aff3jpg", - "url": "https://example.com/images/covers/2b4e4712-5a4d-4ac1-9df4-634cc9c7aff3jpg", - "name": "Cover of \"This Is How You Lose the Time War\"" - } - ], "replies": { "id": "https://example.com/user/mouse/quotation/13/replies", "type": "Collection", diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 0c467c23..ca306bcb 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -14,6 +14,7 @@ from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils import timezone +from django.views.decorators.http import require_GET, require_POST from bookwyrm import books_manager from bookwyrm import forms, models, outgoing @@ -23,11 +24,9 @@ from bookwyrm.settings import DOMAIN from bookwyrm.views import get_user_from_username +@require_GET def user_login(request): ''' authenticate user login ''' - if request.method == 'GET': - return redirect('/login') - login_form = forms.LoginForm(request.POST) username = login_form.data['username'] @@ -50,11 +49,9 @@ def user_login(request): return TemplateResponse(request, 'login.html', data) +@require_GET def register(request): ''' join the server ''' - if request.method == 'GET': - return redirect('/login') - if not models.SiteSettings.get().allow_registration: invite_code = request.POST.get('invite_code') @@ -97,12 +94,14 @@ def register(request): @login_required +@require_GET def user_logout(request): ''' done with this place! outa here! ''' logout(request) return redirect('/') +@require_POST def password_reset_request(request): ''' create a password reset token ''' email = request.POST.get('email') @@ -121,6 +120,7 @@ def password_reset_request(request): return TemplateResponse(request, 'password_reset_request.html', data) +@require_POST def password_reset(request): ''' allow a user to change their password through an emailed token ''' try: @@ -148,6 +148,7 @@ def password_reset(request): @login_required +@require_POST def password_change(request): ''' allow a user to change their password ''' new_password = request.POST.get('password') @@ -163,11 +164,9 @@ def password_change(request): @login_required +@require_POST def edit_profile(request): ''' les get fancy with images ''' - if not request.method == 'POST': - return redirect('/user/%s' % request.user.localname) - form = forms.EditUserForm(request.POST, request.FILES) if not form.is_valid(): data = { @@ -226,11 +225,9 @@ def resolve_book(request): @login_required @permission_required('bookwyrm.edit_book', raise_exception=True) +@require_POST def edit_book(request, book_id): ''' edit a book cool ''' - if not request.method == 'POST': - return redirect('/book/%s' % book_id) - book = get_object_or_404(models.Edition, id=book_id) form = forms.EditionForm(request.POST, request.FILES, instance=book) @@ -248,11 +245,9 @@ def edit_book(request, book_id): @login_required +@require_POST def upload_cover(request, book_id): ''' upload a new cover ''' - if not request.method == 'POST': - return redirect('/') - book = get_object_or_404(models.Edition, id=book_id) form = forms.CoverForm(request.POST, request.FILES, instance=book) @@ -268,6 +263,7 @@ def upload_cover(request, book_id): @login_required +@require_POST @permission_required('bookwyrm.edit_book', raise_exception=True) def add_description(request, book_id): ''' upload a new cover ''' @@ -286,6 +282,7 @@ def add_description(request, book_id): @login_required +@require_POST def create_shelf(request): ''' user generated shelves ''' form = forms.ShelfForm(request.POST) @@ -298,6 +295,7 @@ def create_shelf(request): @login_required +@require_POST def edit_shelf(request, shelf_id): ''' user generated shelves ''' shelf = get_object_or_404(models.Shelf, id=shelf_id) @@ -313,6 +311,7 @@ def edit_shelf(request, shelf_id): @login_required +@require_POST def delete_shelf(request, shelf_id): ''' user generated shelves ''' shelf = get_object_or_404(models.Shelf, id=shelf_id) @@ -324,6 +323,7 @@ def delete_shelf(request, shelf_id): @login_required +@require_POST def shelve(request): ''' put a on a user's shelf ''' book = books_manager.get_edition(request.POST['book']) @@ -358,6 +358,7 @@ def shelve(request): @login_required +@require_POST def unshelve(request): ''' put a on a user's shelf ''' book = models.Edition.objects.get(id=request.POST['book']) @@ -368,6 +369,7 @@ def unshelve(request): @login_required +@require_POST def start_reading(request, book_id): ''' begin reading a book ''' book = books_manager.get_edition(book_id) @@ -403,6 +405,7 @@ def start_reading(request, book_id): @login_required +@require_POST def finish_reading(request, book_id): ''' a user completed a book, yay ''' book = books_manager.get_edition(book_id) @@ -438,6 +441,7 @@ def finish_reading(request, book_id): @login_required +@require_POST def edit_readthrough(request): ''' can't use the form because the dates are too finnicky ''' readthrough = update_readthrough(request, create=False) @@ -453,6 +457,7 @@ def edit_readthrough(request): @login_required +@require_POST def delete_readthrough(request): ''' remove a readthrough ''' readthrough = get_object_or_404( @@ -467,6 +472,7 @@ def delete_readthrough(request): @login_required +@require_POST def rate(request): ''' just a star rating for a book ''' form = forms.RatingForm(request.POST) @@ -474,6 +480,7 @@ def rate(request): @login_required +@require_POST def review(request): ''' create a book review ''' form = forms.ReviewForm(request.POST) @@ -481,6 +488,7 @@ def review(request): @login_required +@require_POST def quotate(request): ''' create a book quotation ''' form = forms.QuotationForm(request.POST) @@ -488,6 +496,7 @@ def quotate(request): @login_required +@require_POST def comment(request): ''' create a book comment ''' form = forms.CommentForm(request.POST) @@ -495,6 +504,7 @@ def comment(request): @login_required +@require_POST def reply(request): ''' respond to a book review ''' form = forms.ReplyForm(request.POST) @@ -511,6 +521,7 @@ def handle_status(request, form): @login_required +@require_POST def tag(request): ''' tag a book ''' # I'm not using a form here because sometimes "name" is sent as a hidden @@ -530,6 +541,7 @@ def tag(request): @login_required +@require_POST def untag(request): ''' untag a book ''' name = request.POST.get('name') @@ -540,6 +552,7 @@ def untag(request): @login_required +@require_POST def favorite(request, status_id): ''' like a status ''' status = models.Status.objects.get(id=status_id) @@ -548,6 +561,7 @@ def favorite(request, status_id): @login_required +@require_POST def unfavorite(request, status_id): ''' like a status ''' status = models.Status.objects.get(id=status_id) @@ -556,6 +570,7 @@ def unfavorite(request, status_id): @login_required +@require_POST def boost(request, status_id): ''' boost a status ''' status = models.Status.objects.get(id=status_id) @@ -564,6 +579,7 @@ def boost(request, status_id): @login_required +@require_POST def unboost(request, status_id): ''' boost a status ''' status = models.Status.objects.get(id=status_id) @@ -572,6 +588,7 @@ def unboost(request, status_id): @login_required +@require_POST def delete_status(request, status_id): ''' delete and tombstone a status ''' status = get_object_or_404(models.Status, id=status_id) @@ -586,6 +603,7 @@ def delete_status(request, status_id): @login_required +@require_POST def follow(request): ''' follow another user, here or abroad ''' username = request.POST['user'] @@ -601,6 +619,7 @@ def follow(request): @login_required +@require_POST def unfollow(request): ''' unfollow a user ''' username = request.POST['user'] @@ -623,6 +642,7 @@ def clear_notifications(request): @login_required +@require_POST def accept_follow_request(request): ''' a user accepts a follow request ''' username = request.POST['user'] @@ -646,6 +666,7 @@ def accept_follow_request(request): @login_required +@require_POST def delete_follow_request(request): ''' a user rejects a follow request ''' username = request.POST['user'] @@ -667,6 +688,7 @@ def delete_follow_request(request): @login_required +@require_POST def import_data(request): ''' ingest a goodreads csv ''' form = forms.ImportForm(request.POST, request.FILES) @@ -690,6 +712,7 @@ def import_data(request): @login_required +@require_POST def retry_import(request): ''' ingest a goodreads csv ''' job = get_object_or_404(models.ImportJob, id=request.POST.get('import_job')) @@ -707,6 +730,7 @@ def retry_import(request): @login_required +@require_POST @permission_required('bookwyrm.create_invites', raise_exception=True) def create_invite(request): ''' creates a user invite database entry ''' diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 38e882cd..e0feaee7 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -11,6 +11,7 @@ from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_GET from bookwyrm import outgoing from bookwyrm.activitypub import ActivityEncoder @@ -47,12 +48,14 @@ def not_found_page(request, _): @login_required +@require_GET def home(request): ''' this is the same as the feed on the home tab ''' return home_tab(request, 'home') @login_required +@require_GET def home_tab(request, tab): ''' user's homepage with activity feed ''' try: @@ -160,6 +163,7 @@ def get_activity_feed(user, filter_level, model=models.Status): return activities +@require_GET def search(request): ''' that search bar up top ''' query = request.GET.get('q') @@ -191,6 +195,7 @@ def search(request): @login_required +@require_GET def import_page(request): ''' import history from goodreads ''' return TemplateResponse(request, 'import.html', { @@ -203,6 +208,7 @@ def import_page(request): @login_required +@require_GET def import_status(request, job_id): ''' status of an import job ''' job = models.ImportJob.objects.get(id=job_id) @@ -221,6 +227,7 @@ def import_status(request, job_id): }) +@require_GET def login_page(request): ''' authentication ''' if request.user.is_authenticated: @@ -235,6 +242,7 @@ def login_page(request): return TemplateResponse(request, 'login.html', data) +@require_GET def about_page(request): ''' more information about the instance ''' data = { @@ -244,6 +252,7 @@ def about_page(request): return TemplateResponse(request, 'about.html', data) +@require_GET def password_reset_request(request): ''' invite management page ''' return TemplateResponse( @@ -253,6 +262,7 @@ def password_reset_request(request): ) +@require_GET def password_reset(request, code): ''' endpoint for sending invites ''' if request.user.is_authenticated: @@ -271,6 +281,7 @@ def password_reset(request, code): ) +@require_GET def invite_page(request, code): ''' endpoint for sending invites ''' if request.user.is_authenticated: @@ -293,6 +304,7 @@ def invite_page(request, code): @login_required @permission_required('bookwyrm.create_invites', raise_exception=True) +@require_GET def manage_invites(request): ''' invite management page ''' data = { @@ -304,6 +316,7 @@ def manage_invites(request): @login_required +@require_GET def notifications_page(request): ''' list notitications ''' notifications = request.user.notification_set.all() \ @@ -319,6 +332,7 @@ def notifications_page(request): @csrf_exempt +@require_GET def user_page(request, username): ''' profile page for a user ''' try: @@ -387,11 +401,9 @@ def user_page(request, username): @csrf_exempt +@require_GET def followers_page(request, username): ''' list of followers ''' - if request.method != 'GET': - return HttpResponseBadRequest() - try: user = get_user_from_username(username) except models.User.DoesNotExist: @@ -410,11 +422,9 @@ def followers_page(request, username): @csrf_exempt +@require_GET def following_page(request, username): ''' list of followers ''' - if request.method != 'GET': - return HttpResponseBadRequest() - try: user = get_user_from_username(username) except models.User.DoesNotExist: @@ -433,11 +443,9 @@ def following_page(request, username): @csrf_exempt +@require_GET def status_page(request, username, status_id): ''' display a particular status (and replies, etc) ''' - if request.method != 'GET': - return HttpResponseBadRequest() - try: user = get_user_from_username(username) status = models.Status.objects.select_subclasses().get(id=status_id) @@ -476,11 +484,9 @@ def status_visible_to_user(viewer, status): @csrf_exempt +@require_GET def replies_page(request, username, status_id): ''' ordered collection of replies to a status ''' - if request.method != 'GET': - return HttpResponseBadRequest() - if not is_api_request(request): return status_page(request, username, status_id) @@ -495,6 +501,7 @@ def replies_page(request, username, status_id): @login_required +@require_GET def edit_profile_page(request): ''' profile page for a user ''' user = request.user @@ -508,6 +515,7 @@ def edit_profile_page(request): return TemplateResponse(request, 'edit_user.html', data) +@require_GET def book_page(request, book_id): ''' info about a book ''' try: @@ -595,6 +603,7 @@ def book_page(request, book_id): @login_required @permission_required('bookwyrm.edit_book', raise_exception=True) +@require_GET def edit_book_page(request, book_id): ''' info about a book ''' book = books_manager.get_edition(book_id) @@ -608,6 +617,7 @@ def edit_book_page(request, book_id): return TemplateResponse(request, 'edit_book.html', data) +@require_GET def editions_page(request, book_id): ''' list of editions of a book ''' work = get_object_or_404(models.Work, id=book_id) @@ -627,6 +637,7 @@ def editions_page(request, book_id): return TemplateResponse(request, 'editions.html', data) +@require_GET def author_page(request, author_id): ''' landing page for an author ''' author = get_object_or_404(models.Author, id=author_id) @@ -643,6 +654,7 @@ def author_page(request, author_id): return TemplateResponse(request, 'author.html', data) +@require_GET def tag_page(request, tag_id): ''' books related to a tag ''' tag_obj = models.Tag.objects.filter(identifier=tag_id).first() @@ -663,11 +675,13 @@ def tag_page(request, tag_id): @csrf_exempt +@require_GET def user_shelves_page(request, username): ''' list of followers ''' return shelf_page(request, username, None) +@require_GET def shelf_page(request, username, shelf_identifier): ''' display a shelf ''' try: