Merge branch 'main' into quick-add-description

This commit is contained in:
Mouse Reeve 2020-11-28 08:36:46 -08:00 committed by GitHub
commit 1c8a0c942a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 248 additions and 171 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 = ''

View file

@ -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):

View file

@ -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):

View file

@ -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',
),
]

View file

@ -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'),
),
]

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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'

View file

@ -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",

View file

@ -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 '''

View file

@ -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: