forked from mirrors/bookwyrm
Merge branch 'main' into html-interaction
This commit is contained in:
commit
f61a25cfb1
72 changed files with 4891 additions and 4146 deletions
|
@ -2,10 +2,11 @@
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from bookwyrm import outgoing
|
from bookwyrm import models
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.broadcast import broadcast
|
||||||
from bookwyrm.models import ImportJob, ImportItem
|
from bookwyrm.models import ImportJob, ImportItem
|
||||||
from bookwyrm.status import create_notification
|
from bookwyrm.status import create_notification
|
||||||
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ def import_data(job_id):
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
# shelves book and handles reviews
|
# shelves book and handles reviews
|
||||||
outgoing.handle_imported_book(
|
handle_imported_book(
|
||||||
job.user, item, job.include_reviews, job.privacy)
|
job.user, item, job.include_reviews, job.privacy)
|
||||||
else:
|
else:
|
||||||
item.fail_reason = 'Could not find a match for book'
|
item.fail_reason = 'Could not find a match for book'
|
||||||
|
@ -71,3 +72,57 @@ def import_data(job_id):
|
||||||
create_notification(job.user, 'IMPORT', related_import=job)
|
create_notification(job.user, 'IMPORT', related_import=job)
|
||||||
job.complete = True
|
job.complete = True
|
||||||
job.save()
|
job.save()
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
if not item.book:
|
||||||
|
return
|
||||||
|
|
||||||
|
existing_shelf = models.ShelfBook.objects.filter(
|
||||||
|
book=item.book, added_by=user).exists()
|
||||||
|
|
||||||
|
# shelve the book if it hasn't been shelved already
|
||||||
|
if item.shelf and not existing_shelf:
|
||||||
|
desired_shelf = models.Shelf.objects.get(
|
||||||
|
identifier=item.shelf,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
shelf_book = models.ShelfBook.objects.create(
|
||||||
|
book=item.book, shelf=desired_shelf, added_by=user)
|
||||||
|
broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)
|
||||||
|
|
||||||
|
for read in item.reads:
|
||||||
|
# check for an existing readthrough with the same dates
|
||||||
|
if models.ReadThrough.objects.filter(
|
||||||
|
user=user, book=item.book,
|
||||||
|
start_date=read.start_date,
|
||||||
|
finish_date=read.finish_date
|
||||||
|
).exists():
|
||||||
|
continue
|
||||||
|
read.book = item.book
|
||||||
|
read.user = user
|
||||||
|
read.save()
|
||||||
|
|
||||||
|
if include_reviews and (item.rating or item.review):
|
||||||
|
review_title = 'Review of {!r} on Goodreads'.format(
|
||||||
|
item.book.title,
|
||||||
|
) if item.review else ''
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
privacy=privacy,
|
||||||
|
)
|
||||||
|
# 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), privacy=privacy)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from bookwyrm import activitypub, models, outgoing
|
from bookwyrm import activitypub, models, views
|
||||||
from bookwyrm import status as status_builder
|
from bookwyrm import status as status_builder
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
from bookwyrm.signatures import Signature
|
from bookwyrm.signatures import Signature
|
||||||
|
@ -133,7 +133,7 @@ def handle_follow(activity):
|
||||||
related_user=relationship.user_subject
|
related_user=relationship.user_subject
|
||||||
)
|
)
|
||||||
if not manually_approves:
|
if not manually_approves:
|
||||||
outgoing.handle_accept(relationship)
|
views.handle_accept(relationship)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
|
|
|
@ -1,414 +0,0 @@
|
||||||
''' handles all the activity coming out of the server '''
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.db import IntegrityError, transaction
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_GET
|
|
||||||
from markdown import markdown
|
|
||||||
from requests import HTTPError
|
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.connectors import get_data, ConnectorException
|
|
||||||
from bookwyrm.broadcast import broadcast
|
|
||||||
from bookwyrm.sanitize_html import InputHtmlParser
|
|
||||||
from bookwyrm.status import create_notification
|
|
||||||
from bookwyrm.status import create_generated_note
|
|
||||||
from bookwyrm.status import delete_status
|
|
||||||
from bookwyrm.settings import DOMAIN
|
|
||||||
from bookwyrm.utils import regex
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_GET
|
|
||||||
def outbox(request, username):
|
|
||||||
''' outbox for the requested user '''
|
|
||||||
user = get_object_or_404(models.User, localname=username)
|
|
||||||
filter_type = request.GET.get('type')
|
|
||||||
if filter_type not in models.status_models:
|
|
||||||
filter_type = None
|
|
||||||
|
|
||||||
return JsonResponse(
|
|
||||||
user.to_outbox(**request.GET, filter_type=filter_type),
|
|
||||||
encoder=activitypub.ActivityEncoder
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_remote_webfinger(query):
|
|
||||||
''' webfingerin' other servers '''
|
|
||||||
user = None
|
|
||||||
|
|
||||||
# usernames could be @user@domain or user@domain
|
|
||||||
if not query:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if query[0] == '@':
|
|
||||||
query = query[1:]
|
|
||||||
|
|
||||||
try:
|
|
||||||
domain = query.split('@')[1]
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = models.User.objects.get(username=query)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \
|
|
||||||
(domain, query)
|
|
||||||
try:
|
|
||||||
data = get_data(url)
|
|
||||||
except (ConnectorException, HTTPError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
for link in data.get('links'):
|
|
||||||
if link.get('rel') == 'self':
|
|
||||||
try:
|
|
||||||
user = activitypub.resolve_remote_id(
|
|
||||||
models.User, link['href']
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def handle_follow(user, to_follow):
|
|
||||||
''' someone local wants to follow someone '''
|
|
||||||
relationship, _ = models.UserFollowRequest.objects.get_or_create(
|
|
||||||
user_subject=user,
|
|
||||||
user_object=to_follow,
|
|
||||||
)
|
|
||||||
activity = relationship.to_activity()
|
|
||||||
broadcast(user, activity, privacy='direct', direct_recipients=[to_follow])
|
|
||||||
|
|
||||||
|
|
||||||
def handle_unfollow(user, to_unfollow):
|
|
||||||
''' someone local wants to follow someone '''
|
|
||||||
relationship = models.UserFollows.objects.get(
|
|
||||||
user_subject=user,
|
|
||||||
user_object=to_unfollow
|
|
||||||
)
|
|
||||||
activity = relationship.to_undo_activity(user)
|
|
||||||
broadcast(user, activity, privacy='direct', direct_recipients=[to_unfollow])
|
|
||||||
to_unfollow.followers.remove(user)
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
relationship.save()
|
|
||||||
|
|
||||||
activity = relationship.to_accept_activity()
|
|
||||||
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
|
|
||||||
|
|
||||||
|
|
||||||
def handle_reject(follow_request):
|
|
||||||
''' a local user who managed follows rejects a follow request '''
|
|
||||||
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])
|
|
||||||
|
|
||||||
|
|
||||||
def handle_shelve(user, book, shelf):
|
|
||||||
''' a local user is getting a book put on their shelf '''
|
|
||||||
# update the database
|
|
||||||
shelve = models.ShelfBook(book=book, shelf=shelf, added_by=user)
|
|
||||||
shelve.save()
|
|
||||||
|
|
||||||
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 = {
|
|
||||||
'to-read': 'wants to read',
|
|
||||||
'reading': 'started reading',
|
|
||||||
'read': 'finished reading'
|
|
||||||
}[shelf.identifier]
|
|
||||||
except KeyError:
|
|
||||||
# it's a non-standard shelf, don't worry about it
|
|
||||||
return
|
|
||||||
|
|
||||||
status = create_generated_note(
|
|
||||||
user,
|
|
||||||
message,
|
|
||||||
mention_books=[book],
|
|
||||||
privacy=privacy
|
|
||||||
)
|
|
||||||
status.save()
|
|
||||||
|
|
||||||
broadcast(user, status.to_create_activity(user))
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
if not item.book:
|
|
||||||
return
|
|
||||||
|
|
||||||
existing_shelf = models.ShelfBook.objects.filter(
|
|
||||||
book=item.book, added_by=user).exists()
|
|
||||||
|
|
||||||
# shelve the book if it hasn't been shelved already
|
|
||||||
if item.shelf and not existing_shelf:
|
|
||||||
desired_shelf = models.Shelf.objects.get(
|
|
||||||
identifier=item.shelf,
|
|
||||||
user=user
|
|
||||||
)
|
|
||||||
shelf_book = models.ShelfBook.objects.create(
|
|
||||||
book=item.book, shelf=desired_shelf, added_by=user)
|
|
||||||
broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)
|
|
||||||
|
|
||||||
for read in item.reads:
|
|
||||||
# check for an existing readthrough with the same dates
|
|
||||||
if models.ReadThrough.objects.filter(
|
|
||||||
user=user, book=item.book,
|
|
||||||
start_date=read.start_date,
|
|
||||||
finish_date=read.finish_date
|
|
||||||
).exists():
|
|
||||||
continue
|
|
||||||
read.book = item.book
|
|
||||||
read.user = user
|
|
||||||
read.save()
|
|
||||||
|
|
||||||
if include_reviews and (item.rating or item.review):
|
|
||||||
review_title = 'Review of {!r} on Goodreads'.format(
|
|
||||||
item.book.title,
|
|
||||||
) if item.review else ''
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
privacy=privacy,
|
|
||||||
)
|
|
||||||
# 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), privacy=privacy)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_delete_status(user, status):
|
|
||||||
''' delete a status and broadcast deletion to other servers '''
|
|
||||||
delete_status(status)
|
|
||||||
broadcast(user, status.to_delete_activity(user))
|
|
||||||
|
|
||||||
|
|
||||||
def handle_status(user, form):
|
|
||||||
''' generic handler for statuses '''
|
|
||||||
status = form.save(commit=False)
|
|
||||||
if not status.sensitive and status.content_warning:
|
|
||||||
# the cw text field remains populated when you click "remove"
|
|
||||||
status.content_warning = None
|
|
||||||
status.save()
|
|
||||||
|
|
||||||
# inspect the text for user tags
|
|
||||||
content = status.content
|
|
||||||
for (mention_text, mention_user) in find_mentions(content):
|
|
||||||
# add them to status mentions fk
|
|
||||||
status.mention_users.add(mention_user)
|
|
||||||
|
|
||||||
# turn the mention into a link
|
|
||||||
content = re.sub(
|
|
||||||
r'%s([^@]|$)' % mention_text,
|
|
||||||
r'<a href="%s">%s</a>\g<1>' % \
|
|
||||||
(mention_user.remote_id, mention_text),
|
|
||||||
content)
|
|
||||||
|
|
||||||
# add reply parent to mentions and notify
|
|
||||||
if status.reply_parent:
|
|
||||||
status.mention_users.add(status.reply_parent.user)
|
|
||||||
for mention_user in status.reply_parent.mention_users.all():
|
|
||||||
status.mention_users.add(mention_user)
|
|
||||||
|
|
||||||
if status.reply_parent.user.local:
|
|
||||||
create_notification(
|
|
||||||
status.reply_parent.user,
|
|
||||||
'REPLY',
|
|
||||||
related_user=user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
# deduplicate mentions
|
|
||||||
status.mention_users.set(set(status.mention_users.all()))
|
|
||||||
# create mention notifications
|
|
||||||
for mention_user in status.mention_users.all():
|
|
||||||
if status.reply_parent and mention_user == status.reply_parent.user:
|
|
||||||
continue
|
|
||||||
if mention_user.local:
|
|
||||||
create_notification(
|
|
||||||
mention_user,
|
|
||||||
'MENTION',
|
|
||||||
related_user=user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
# don't apply formatting to generated notes
|
|
||||||
if not isinstance(status, models.GeneratedNote):
|
|
||||||
status.content = to_markdown(content)
|
|
||||||
# do apply formatting to quotes
|
|
||||||
if hasattr(status, 'quote'):
|
|
||||||
status.quote = to_markdown(status.quote)
|
|
||||||
|
|
||||||
status.save()
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
def find_mentions(content):
|
|
||||||
''' detect @mentions in raw status content '''
|
|
||||||
for match in re.finditer(regex.strict_username, content):
|
|
||||||
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)
|
|
||||||
|
|
||||||
mention_user = handle_remote_webfinger(username)
|
|
||||||
if not mention_user:
|
|
||||||
# we can ignore users we don't know about
|
|
||||||
continue
|
|
||||||
yield (match.group(), mention_user)
|
|
||||||
|
|
||||||
|
|
||||||
def format_links(content):
|
|
||||||
''' detect and format links '''
|
|
||||||
return re.sub(
|
|
||||||
r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
|
|
||||||
regex.domain,
|
|
||||||
r'\g<1><a href="\g<2>">\g<3></a>',
|
|
||||||
content)
|
|
||||||
|
|
||||||
def to_markdown(content):
|
|
||||||
''' catch links and convert to markdown '''
|
|
||||||
content = format_links(content)
|
|
||||||
content = markdown(content)
|
|
||||||
# sanitize resulting html
|
|
||||||
sanitizer = InputHtmlParser()
|
|
||||||
sanitizer.feed(content)
|
|
||||||
return sanitizer.get_output()
|
|
||||||
|
|
||||||
|
|
||||||
def handle_favorite(user, status):
|
|
||||||
''' a user likes a status '''
|
|
||||||
try:
|
|
||||||
favorite = models.Favorite.objects.create(
|
|
||||||
status=status,
|
|
||||||
user=user
|
|
||||||
)
|
|
||||||
except IntegrityError:
|
|
||||||
# you already fav'ed that
|
|
||||||
return
|
|
||||||
|
|
||||||
fav_activity = favorite.to_activity()
|
|
||||||
broadcast(
|
|
||||||
user, fav_activity, privacy='direct', direct_recipients=[status.user])
|
|
||||||
if status.user.local:
|
|
||||||
create_notification(
|
|
||||||
status.user,
|
|
||||||
'FAVORITE',
|
|
||||||
related_user=user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_unfavorite(user, status):
|
|
||||||
''' a user likes a status '''
|
|
||||||
try:
|
|
||||||
favorite = models.Favorite.objects.get(
|
|
||||||
status=status,
|
|
||||||
user=user
|
|
||||||
)
|
|
||||||
except models.Favorite.DoesNotExist:
|
|
||||||
# can't find that status, idk
|
|
||||||
return
|
|
||||||
|
|
||||||
fav_activity = favorite.to_undo_activity(user)
|
|
||||||
favorite.delete()
|
|
||||||
broadcast(user, fav_activity, direct_recipients=[status.user])
|
|
||||||
|
|
||||||
# check for notification
|
|
||||||
if status.user.local:
|
|
||||||
notification = models.Notification.objects.filter(
|
|
||||||
user=status.user, related_user=user,
|
|
||||||
related_status=status, notification_type='FAVORITE'
|
|
||||||
).first()
|
|
||||||
if notification:
|
|
||||||
notification.delete()
|
|
||||||
|
|
||||||
|
|
||||||
def handle_boost(user, status):
|
|
||||||
''' a user wishes to boost a status '''
|
|
||||||
# is it boostable?
|
|
||||||
if not status.boostable:
|
|
||||||
return
|
|
||||||
|
|
||||||
if models.Boost.objects.filter(
|
|
||||||
boosted_status=status, user=user).exists():
|
|
||||||
# you already boosted that.
|
|
||||||
return
|
|
||||||
boost = models.Boost.objects.create(
|
|
||||||
boosted_status=status,
|
|
||||||
privacy=status.privacy,
|
|
||||||
user=user,
|
|
||||||
)
|
|
||||||
|
|
||||||
boost_activity = boost.to_activity()
|
|
||||||
broadcast(user, boost_activity)
|
|
||||||
|
|
||||||
if status.user.local:
|
|
||||||
create_notification(
|
|
||||||
status.user,
|
|
||||||
'BOOST',
|
|
||||||
related_user=user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_unboost(user, status):
|
|
||||||
''' a user regrets boosting a status '''
|
|
||||||
boost = models.Boost.objects.filter(
|
|
||||||
boosted_status=status, user=user
|
|
||||||
).first()
|
|
||||||
activity = boost.to_undo_activity(user)
|
|
||||||
|
|
||||||
boost.delete()
|
|
||||||
broadcast(user, activity)
|
|
||||||
|
|
||||||
# delete related notification
|
|
||||||
if status.user.local:
|
|
||||||
notification = models.Notification.objects.filter(
|
|
||||||
user=status.user, related_user=user,
|
|
||||||
related_status=status, notification_type='BOOST'
|
|
||||||
).first()
|
|
||||||
if notification:
|
|
||||||
notification.delete()
|
|
|
@ -9,6 +9,9 @@
|
||||||
.card {
|
.card {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
.card-header-title {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- TOGGLES --- */
|
/* --- TOGGLES --- */
|
||||||
input.toggle-control {
|
input.toggle-control {
|
||||||
|
|
|
@ -223,7 +223,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block" id="reviews">
|
||||||
{% for review in reviews %}
|
{% for review in reviews %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% include 'snippets/status.html' with status=review hide_book=True depth=1 %}
|
{% include 'snippets/status.html' with status=review hide_book=True depth=1 %}
|
||||||
|
@ -231,25 +231,28 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="block is-flex is-flex-wrap-wrap">
|
<div class="block is-flex is-flex-wrap-wrap">
|
||||||
{% for rating in ratings %}
|
{% for rating in ratings %}
|
||||||
<div class="block mr-5">
|
<div class="block mr-5">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">{% include 'snippets/avatar.html' with user=rating.user %}</div>
|
<div class="media-left">{% include 'snippets/avatar.html' with user=rating.user %}</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div>
|
<div>
|
||||||
{% include 'snippets/username.html' with user=rating.user %}
|
{% include 'snippets/username.html' with user=rating.user %}
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped mb-0">
|
<div class="field is-grouped mb-0">
|
||||||
<div>rated it</div>
|
<div>rated it</div>
|
||||||
{% include 'snippets/stars.html' with rating=rating.rating %}
|
{% include 'snippets/stars.html' with rating=rating.rating %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
|
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<div class="block">
|
||||||
|
{% include 'snippets/pagination.html' with page=reviews path=book.local_path anchor="#reviews" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -13,25 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
{% include 'snippets/pagination.html' with page=activities path="direct-messages" %}
|
||||||
{% if prev %}
|
|
||||||
<p class="pagination-previous">
|
|
||||||
<a href="{{ prev }}">
|
|
||||||
<span class="icon icon-arrow-left"></span>
|
|
||||||
Previous
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if next %}
|
|
||||||
<p class="pagination-next">
|
|
||||||
<a href="{{ next }}">
|
|
||||||
Next
|
|
||||||
<span class="icon icon-arrow-right"></span>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<div class="tile is-child box has-background-primary-light content">
|
<div class="tile is-child box has-background-primary-light content">
|
||||||
{% if site.allow_registration %}
|
{% if site.allow_registration %}
|
||||||
<h2 class="title">Join {{ site.name }}</h2>
|
<h2 class="title">Join {{ site.name }}</h2>
|
||||||
<form name="register" method="post" action="/user-register">
|
<form name="register" method="post" action="/register">
|
||||||
{% include 'snippets/register_form.html' %}
|
{% include 'snippets/register_form.html' %}
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="block" name="edit-author" action="/edit-author/{{ author.id }}" method="post">
|
<form class="block" name="edit-author" action="{{ author.local_path }}/edit" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="block" name="edit-book" action="/edit-book/{{ book.id }}" method="post" enctype="multipart/form-data">
|
<form class="block" name="edit-book" action="{{ book.local_path }}/edit" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
|
|
@ -94,25 +94,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
{% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %}
|
||||||
{% if prev %}
|
|
||||||
<p class="pagination-previous">
|
|
||||||
<a href="{{ prev }}">
|
|
||||||
<span class="icon icon-arrow-left"></span>
|
|
||||||
Previous
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if next %}
|
|
||||||
<p class="pagination-next">
|
|
||||||
<a href="{{ next }}">
|
|
||||||
Next
|
|
||||||
<span class="icon icon-arrow-right"></span>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h1 class="title">Import Books from GoodReads</h1>
|
<h1 class="title">Import Books from GoodReads</h1>
|
||||||
<form name="import" action="/import-data/" method="post" enctype="multipart/form-data">
|
<form name="import" action="/import" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{ import_form.as_p }}
|
{{ import_form.as_p }}
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<li><a href="/import-status/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li>
|
<li><a href="/import/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,9 +30,8 @@
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h2 class="title is-4">Failed to load</h2>
|
<h2 class="title is-4">Failed to load</h2>
|
||||||
{% if not job.retry %}
|
{% if not job.retry %}
|
||||||
<form name="retry" action="/retry-import/" method="post">
|
<form name="retry" action="/import/{{ job.id }}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="import_job" value="{{ job.id }}">
|
|
||||||
<ul>
|
<ul>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% for item in failed_items %}
|
{% for item in failed_items %}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
{% if valid %}
|
{% if valid %}
|
||||||
<h1 class="title">Create an Account</h1>
|
<h1 class="title">Create an Account</h1>
|
||||||
<div>
|
<div>
|
||||||
<form name="register" method="post" action="/user-register">
|
<form name="register" method="post" action="/register">
|
||||||
<input type=hidden name="invite_code" value="{{ invite.code }}">
|
<input type=hidden name="invite_code" value="{{ invite.code }}">
|
||||||
{% include 'snippets/register_form.html' %}
|
{% include 'snippets/register_form.html' %}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/user-edit" class="navbar-item">
|
<a href="/edit-profile" class="navbar-item">
|
||||||
Settings
|
Settings
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -122,10 +122,10 @@
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
{% if request.path != '/login' and request.path != '/login/' and request.path != '/user-login' %}
|
{% if request.path != '/login' and request.path != '/login/' %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<form name="login" method="post" action="/user-login">
|
<form name="login" method="post" action="/login">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="columns is-variable is-1">
|
<div class="columns is-variable is-1">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
{% if login_form.non_field_errors %}
|
{% if login_form.non_field_errors %}
|
||||||
<p class="notification is-danger">{{ login_form.non_field_errors }}</p>
|
<p class="notification is-danger">{{ login_form.non_field_errors }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form name="login" method="post" action="/user-login">
|
<form name="login" method="post" action="/login">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_localname">Username:</label>
|
<label class="label" for="id_localname">Username:</label>
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<div class="box has-background-primary-light">
|
<div class="box has-background-primary-light">
|
||||||
{% if site.allow_registration %}
|
{% if site.allow_registration %}
|
||||||
<h2 class="title">Create an Account</h2>
|
<h2 class="title">Create an Account</h2>
|
||||||
<form name="register" method="post" action="/user-register">
|
<form name="register" method="post" action="/register">
|
||||||
{% include 'snippets/register_form.html' %}
|
{% include 'snippets/register_form.html' %}
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h2 class="title is-4">Generate New Invite</h2>
|
<h2 class="title is-4">Generate New Invite</h2>
|
||||||
|
|
||||||
<form name="invite" action="/create-invite/" method="post">
|
<form name="invite" action="/invite/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h1 class="title">Notifications</h1>
|
<h1 class="title">Notifications</h1>
|
||||||
|
|
||||||
<form name="clear" action="/clear-notifications" method="POST">
|
<form name="clear" action="/notifications" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-danger is-light" type="submit" class="secondary">Delete notifications</button>
|
<button class="button is-danger is-light" type="submit" class="secondary">Delete notifications</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
boosted your <a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
boosted your <a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
your <a href="/import-status/{{ notification.related_import.id }}">import</a> completed.
|
your <a href="/import/{{ notification.related_import.id }}">import</a> completed.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
{% for error in errors %}
|
{% for error in errors %}
|
||||||
<p class="is-danger">{{ error }}</p>
|
<p class="is-danger">{{ error }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<form name="reset-password" method="post" action="/reset-password">
|
<form name="password-reset" method="post" action="/password-reset/{{ code }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="reset-code" value="{{ code }}">
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_password">Password:</label>
|
<label class="label" for="id_password">Password:</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<h1 class="title">Reset Password</h1>
|
<h1 class="title">Reset Password</h1>
|
||||||
{% if message %}<p>{{ message }}</p>{% endif %}
|
{% if message %}<p>{{ message }}</p>{% endif %}
|
||||||
<p>A link to reset your password will be sent to your email address</p>
|
<p>A link to reset your password will be sent to your email address</p>
|
||||||
<form name="reset-password" method="post" action="/reset-password-request">
|
<form name="password-reset" method="post" action="/password-reset">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_email_register">Email address:</label>
|
<label class="label" for="id_email_register">Email address:</label>
|
||||||
|
|
|
@ -39,5 +39,5 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input class="toggle-control" type="radio" name="status-tabs-{{ book.id }}" id="quote-{{ book.id }}">
|
<input class="toggle-control" type="radio" name="status-tabs-{{ book.id }}" id="quote-{{ book.id }}">
|
||||||
{% include 'snippets/create_status_form.html' with type="quote" placeholder="An excerpt from '"|add:book.title|add:"'" %}
|
{% include 'snippets/create_status_form.html' with type="quotation" placeholder="An excerpt from '"|add:book.title|add:"'" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<form class="toggle-content hidden tab-option-{{ book.id }}" name="{{ type }}" action="/{{ type }}" method="post" id="tab-{{ type }}-{{ book.id }}">
|
<form class="toggle-content hidden tab-option-{{ book.id }}" name="{{ type }}" action="/post/{{ type }}" method="post" id="tab-{{ type }}-{{ book.id }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="label" for="id_{% if type == 'quote' %}quote{% else %}content{% endif %}_{{ book.id }}_{{ type }}">{{ type|title }}:</label>
|
<label class="label" for="id_{% if type == 'quotation' %}quote{% else %}content{% endif %}_{{ book.id }}_{{ type }}">{{ type|title }}:</label>
|
||||||
{% include 'snippets/content_warning_field.html' %}
|
{% include 'snippets/content_warning_field.html' %}
|
||||||
|
|
||||||
{% if type == 'review' %}
|
{% if type == 'review' %}
|
||||||
|
@ -28,13 +28,13 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if type == 'quote' %}
|
{% if type == 'quotation' %}
|
||||||
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
|
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
|
||||||
{% else %}
|
{% else %}
|
||||||
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
|
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if type == 'quote' %}
|
{% if type == 'quotation' %}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="label" for="id_content_{{ book.id }}_quote">Comment:</label>
|
<label class="label" for="id_content_{{ book.id }}_quote">Comment:</label>
|
||||||
<textarea name="content" class="textarea is-small" id="id_content_{{ book.id }}_quote"></textarea>
|
<textarea name="content" class="textarea is-small" id="id_content_{{ book.id }}_quote"></textarea>
|
||||||
|
|
19
bookwyrm/templates/snippets/pagination.html
Normal file
19
bookwyrm/templates/snippets/pagination.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||||
|
{% if page.has_previous %}
|
||||||
|
<p class="pagination-previous">
|
||||||
|
<a href="{{ path }}?page={{ page.previous_page_number }}{{ anchor }}">
|
||||||
|
<span class="icon icon-arrow-left"></span>
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page.has_next %}
|
||||||
|
<p class="pagination-next">
|
||||||
|
<a href="{{ path }}?page={{ page.next_page_number }}{{ anchor }}">
|
||||||
|
Next
|
||||||
|
<span class="icon icon-arrow-right"></span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
|
@ -1,5 +1,6 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
<form class="is-flex-grow-1" name="reply" action="/reply" method="post">
|
{% with status.id|uuid as uuid %}
|
||||||
|
<form class="is-flex-grow-1" name="reply" action="/post/reply" method="post">
|
||||||
<div class="columns is-align-items-flex-end">
|
<div class="columns is-align-items-flex-end">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="reply_parent" value="{{ status.id }}">
|
<input type="hidden" name="reply_parent" value="{{ status.id }}">
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% if is_self %}
|
{% if is_self %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a href="/user-edit/">
|
<a href="/edit-profile">
|
||||||
<span class="icon icon-pencil" title="Edit profile">
|
<span class="icon icon-pencil" title="Edit profile">
|
||||||
<span class="is-sr-only">Edit profile</span>
|
<span class="is-sr-only">Edit profile</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
<h2 class="title">User Activity</h2>
|
<h2 class="title">User Activity</h2>
|
||||||
</div>
|
</div>
|
||||||
{% for activity in activities %}
|
{% for activity in activities %}
|
||||||
<div class="block">
|
<div class="block" id="feed">
|
||||||
{% include 'snippets/status.html' with status=activity %}
|
{% include 'snippets/status.html' with status=activity %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -55,25 +55,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
{% include 'snippets/pagination.html' with page=activities path=user.local_path anchor="#feed" %}
|
||||||
{% if prev %}
|
|
||||||
<p class="pagination-previous">
|
|
||||||
<a href="{{ prev }}">
|
|
||||||
<span class="icon icon-arrow-left"></span>
|
|
||||||
Previous
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if next %}
|
|
||||||
<p class="pagination-next">
|
|
||||||
<a href="{{ next }}">
|
|
||||||
Next
|
|
||||||
<span class="icon icon-arrow-right"></span>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django import template
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.outgoing import to_markdown
|
from bookwyrm.views.status import to_markdown
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
''' testing import '''
|
''' testing import '''
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
import csv
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
@ -30,6 +31,12 @@ class GoodreadsImport(TestCase):
|
||||||
search_url='https://%s/search?q=' % DOMAIN,
|
search_url='https://%s/search?q=' % DOMAIN,
|
||||||
priority=1,
|
priority=1,
|
||||||
)
|
)
|
||||||
|
work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=work
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_job(self):
|
def test_create_job(self):
|
||||||
|
@ -97,8 +104,140 @@ class GoodreadsImport(TestCase):
|
||||||
'bookwyrm.models.import_job.ImportItem.get_book_from_isbn'
|
'bookwyrm.models.import_job.ImportItem.get_book_from_isbn'
|
||||||
) as resolve:
|
) as resolve:
|
||||||
resolve.return_value = book
|
resolve.return_value = book
|
||||||
with patch('bookwyrm.outgoing.handle_imported_book'):
|
with patch('bookwyrm.goodreads_import.handle_imported_book'):
|
||||||
goodreads_import.import_data(import_job.id)
|
goodreads_import.import_data(import_job.id)
|
||||||
|
|
||||||
import_item = models.ImportItem.objects.get(job=import_job, index=0)
|
import_item = models.ImportItem.objects.get(job=import_job, index=0)
|
||||||
self.assertEqual(import_item.book.id, book.id)
|
self.assertEqual(import_item.book.id, book.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_imported_book(self):
|
||||||
|
''' goodreads import added a book, this adds related connections '''
|
||||||
|
shelf = self.user.shelf_set.filter(identifier='read').first()
|
||||||
|
self.assertIsNone(shelf.books.first())
|
||||||
|
|
||||||
|
import_job = models.ImportJob.objects.create(user=self.user)
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
||||||
|
csv_file = open(datafile, 'r')
|
||||||
|
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
||||||
|
import_item = models.ImportItem.objects.create(
|
||||||
|
job_id=import_job.id, index=index, data=entry, book=self.book)
|
||||||
|
break
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
goodreads_import.handle_imported_book(
|
||||||
|
self.user, import_item, False, 'public')
|
||||||
|
|
||||||
|
shelf.refresh_from_db()
|
||||||
|
self.assertEqual(shelf.books.first(), self.book)
|
||||||
|
|
||||||
|
readthrough = models.ReadThrough.objects.get(user=self.user)
|
||||||
|
self.assertEqual(readthrough.book, self.book)
|
||||||
|
# I can't remember how to create dates and I don't want to look it up.
|
||||||
|
self.assertEqual(readthrough.start_date.year, 2020)
|
||||||
|
self.assertEqual(readthrough.start_date.month, 10)
|
||||||
|
self.assertEqual(readthrough.start_date.day, 21)
|
||||||
|
self.assertEqual(readthrough.finish_date.year, 2020)
|
||||||
|
self.assertEqual(readthrough.finish_date.month, 10)
|
||||||
|
self.assertEqual(readthrough.finish_date.day, 25)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_imported_book_already_shelved(self):
|
||||||
|
''' goodreads import added a book, this adds related connections '''
|
||||||
|
shelf = self.user.shelf_set.filter(identifier='to-read').first()
|
||||||
|
models.ShelfBook.objects.create(
|
||||||
|
shelf=shelf, added_by=self.user, book=self.book)
|
||||||
|
|
||||||
|
import_job = models.ImportJob.objects.create(user=self.user)
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
||||||
|
csv_file = open(datafile, 'r')
|
||||||
|
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
||||||
|
import_item = models.ImportItem.objects.create(
|
||||||
|
job_id=import_job.id, index=index, data=entry, book=self.book)
|
||||||
|
break
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
goodreads_import.handle_imported_book(
|
||||||
|
self.user, import_item, False, 'public')
|
||||||
|
|
||||||
|
shelf.refresh_from_db()
|
||||||
|
self.assertEqual(shelf.books.first(), self.book)
|
||||||
|
self.assertIsNone(
|
||||||
|
self.user.shelf_set.get(identifier='read').books.first())
|
||||||
|
readthrough = models.ReadThrough.objects.get(user=self.user)
|
||||||
|
self.assertEqual(readthrough.book, self.book)
|
||||||
|
self.assertEqual(readthrough.start_date.year, 2020)
|
||||||
|
self.assertEqual(readthrough.start_date.month, 10)
|
||||||
|
self.assertEqual(readthrough.start_date.day, 21)
|
||||||
|
self.assertEqual(readthrough.finish_date.year, 2020)
|
||||||
|
self.assertEqual(readthrough.finish_date.month, 10)
|
||||||
|
self.assertEqual(readthrough.finish_date.day, 25)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_import_twice(self):
|
||||||
|
''' re-importing books '''
|
||||||
|
shelf = self.user.shelf_set.filter(identifier='read').first()
|
||||||
|
import_job = models.ImportJob.objects.create(user=self.user)
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
||||||
|
csv_file = open(datafile, 'r')
|
||||||
|
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
||||||
|
import_item = models.ImportItem.objects.create(
|
||||||
|
job_id=import_job.id, index=index, data=entry, book=self.book)
|
||||||
|
break
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
goodreads_import.handle_imported_book(
|
||||||
|
self.user, import_item, False, 'public')
|
||||||
|
goodreads_import.handle_imported_book(
|
||||||
|
self.user, import_item, False, 'public')
|
||||||
|
|
||||||
|
shelf.refresh_from_db()
|
||||||
|
self.assertEqual(shelf.books.first(), self.book)
|
||||||
|
|
||||||
|
readthrough = models.ReadThrough.objects.get(user=self.user)
|
||||||
|
self.assertEqual(readthrough.book, self.book)
|
||||||
|
# I can't remember how to create dates and I don't want to look it up.
|
||||||
|
self.assertEqual(readthrough.start_date.year, 2020)
|
||||||
|
self.assertEqual(readthrough.start_date.month, 10)
|
||||||
|
self.assertEqual(readthrough.start_date.day, 21)
|
||||||
|
self.assertEqual(readthrough.finish_date.year, 2020)
|
||||||
|
self.assertEqual(readthrough.finish_date.month, 10)
|
||||||
|
self.assertEqual(readthrough.finish_date.day, 25)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_imported_book_review(self):
|
||||||
|
''' goodreads review import '''
|
||||||
|
import_job = models.ImportJob.objects.create(user=self.user)
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
||||||
|
csv_file = open(datafile, 'r')
|
||||||
|
entry = list(csv.DictReader(csv_file))[2]
|
||||||
|
import_item = models.ImportItem.objects.create(
|
||||||
|
job_id=import_job.id, index=0, data=entry, book=self.book)
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
goodreads_import.handle_imported_book(
|
||||||
|
self.user, import_item, True, 'unlisted')
|
||||||
|
review = models.Review.objects.get(book=self.book, user=self.user)
|
||||||
|
self.assertEqual(review.content, 'mixed feelings')
|
||||||
|
self.assertEqual(review.rating, 2)
|
||||||
|
self.assertEqual(review.published_date.year, 2019)
|
||||||
|
self.assertEqual(review.published_date.month, 7)
|
||||||
|
self.assertEqual(review.published_date.day, 8)
|
||||||
|
self.assertEqual(review.privacy, 'unlisted')
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_imported_book_reviews_disabled(self):
|
||||||
|
''' goodreads review import '''
|
||||||
|
import_job = models.ImportJob.objects.create(user=self.user)
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
||||||
|
csv_file = open(datafile, 'r')
|
||||||
|
entry = list(csv.DictReader(csv_file))[2]
|
||||||
|
import_item = models.ImportItem.objects.create(
|
||||||
|
job_id=import_job.id, index=0, data=entry, book=self.book)
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
goodreads_import.handle_imported_book(
|
||||||
|
self.user, import_item, False, 'unlisted')
|
||||||
|
self.assertFalse(models.Review.objects.filter(
|
||||||
|
book=self.book, user=self.user
|
||||||
|
).exists())
|
||||||
|
|
|
@ -1,705 +0,0 @@
|
||||||
''' sending out activities '''
|
|
||||||
import csv
|
|
||||||
import json
|
|
||||||
import pathlib
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
import responses
|
|
||||||
|
|
||||||
from bookwyrm import forms, models, outgoing
|
|
||||||
from bookwyrm.settings import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
|
||||||
class Outgoing(TestCase):
|
|
||||||
''' sends out activities '''
|
|
||||||
def setUp(self):
|
|
||||||
''' we'll need some data '''
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
with patch('bookwyrm.models.user.set_remote_server'):
|
|
||||||
self.remote_user = models.User.objects.create_user(
|
|
||||||
'rat', 'rat@email.com', 'ratword',
|
|
||||||
local=False,
|
|
||||||
remote_id='https://example.com/users/rat',
|
|
||||||
inbox='https://example.com/users/rat/inbox',
|
|
||||||
outbox='https://example.com/users/rat/outbox',
|
|
||||||
)
|
|
||||||
self.local_user = models.User.objects.create_user(
|
|
||||||
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
|
||||||
local=True, localname='mouse',
|
|
||||||
remote_id='https://example.com/users/mouse',
|
|
||||||
)
|
|
||||||
|
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath(
|
|
||||||
'data/ap_user.json'
|
|
||||||
)
|
|
||||||
self.userdata = json.loads(datafile.read_bytes())
|
|
||||||
del self.userdata['icon']
|
|
||||||
|
|
||||||
work = models.Work.objects.create(title='Test Work')
|
|
||||||
self.book = models.Edition.objects.create(
|
|
||||||
title='Example Edition',
|
|
||||||
remote_id='https://example.com/book/1',
|
|
||||||
parent_work=work
|
|
||||||
)
|
|
||||||
self.shelf = models.Shelf.objects.create(
|
|
||||||
name='Test Shelf',
|
|
||||||
identifier='test-shelf',
|
|
||||||
user=self.local_user
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_outbox(self):
|
|
||||||
''' returns user's statuses '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
result = outgoing.outbox(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, JsonResponse)
|
|
||||||
|
|
||||||
def test_outbox_bad_method(self):
|
|
||||||
''' can't POST to outbox '''
|
|
||||||
request = self.factory.post('')
|
|
||||||
result = outgoing.outbox(request, 'mouse')
|
|
||||||
self.assertEqual(result.status_code, 405)
|
|
||||||
|
|
||||||
def test_outbox_unknown_user(self):
|
|
||||||
''' should 404 for unknown and remote users '''
|
|
||||||
request = self.factory.post('')
|
|
||||||
result = outgoing.outbox(request, 'beepboop')
|
|
||||||
self.assertEqual(result.status_code, 405)
|
|
||||||
result = outgoing.outbox(request, 'rat')
|
|
||||||
self.assertEqual(result.status_code, 405)
|
|
||||||
|
|
||||||
def test_outbox_privacy(self):
|
|
||||||
''' don't show dms et cetera in outbox '''
|
|
||||||
models.Status.objects.create(
|
|
||||||
content='PRIVATE!!', user=self.local_user, privacy='direct')
|
|
||||||
models.Status.objects.create(
|
|
||||||
content='bffs ONLY', user=self.local_user, privacy='followers')
|
|
||||||
models.Status.objects.create(
|
|
||||||
content='unlisted status', user=self.local_user, privacy='unlisted')
|
|
||||||
models.Status.objects.create(
|
|
||||||
content='look at this', user=self.local_user, privacy='public')
|
|
||||||
|
|
||||||
request = self.factory.get('')
|
|
||||||
result = outgoing.outbox(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, JsonResponse)
|
|
||||||
data = json.loads(result.content)
|
|
||||||
self.assertEqual(data['type'], 'OrderedCollection')
|
|
||||||
self.assertEqual(data['totalItems'], 2)
|
|
||||||
|
|
||||||
def test_outbox_filter(self):
|
|
||||||
''' if we only care about reviews, only get reviews '''
|
|
||||||
models.Review.objects.create(
|
|
||||||
content='look at this', name='hi', rating=1,
|
|
||||||
book=self.book, user=self.local_user)
|
|
||||||
models.Status.objects.create(
|
|
||||||
content='look at this', user=self.local_user)
|
|
||||||
|
|
||||||
request = self.factory.get('', {'type': 'bleh'})
|
|
||||||
result = outgoing.outbox(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, JsonResponse)
|
|
||||||
data = json.loads(result.content)
|
|
||||||
self.assertEqual(data['type'], 'OrderedCollection')
|
|
||||||
self.assertEqual(data['totalItems'], 2)
|
|
||||||
|
|
||||||
request = self.factory.get('', {'type': 'Review'})
|
|
||||||
result = outgoing.outbox(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, JsonResponse)
|
|
||||||
data = json.loads(result.content)
|
|
||||||
self.assertEqual(data['type'], 'OrderedCollection')
|
|
||||||
self.assertEqual(data['totalItems'], 1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_follow(self):
|
|
||||||
''' send a follow request '''
|
|
||||||
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
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):
|
|
||||||
''' send an unfollow '''
|
|
||||||
self.remote_user.followers.add(self.local_user)
|
|
||||||
self.assertEqual(self.remote_user.followers.count(), 1)
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_unfollow(self.local_user, self.remote_user)
|
|
||||||
|
|
||||||
self.assertEqual(self.remote_user.followers.count(), 0)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_accept(self):
|
|
||||||
''' accept a follow request '''
|
|
||||||
rel = models.UserFollowRequest.objects.create(
|
|
||||||
user_subject=self.local_user,
|
|
||||||
user_object=self.remote_user
|
|
||||||
)
|
|
||||||
rel_id = rel.id
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_reject(self):
|
|
||||||
''' reject a follow request '''
|
|
||||||
rel = models.UserFollowRequest.objects.create(
|
|
||||||
user_subject=self.local_user,
|
|
||||||
user_object=self.remote_user
|
|
||||||
)
|
|
||||||
rel_id = rel.id
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_existing_user(self):
|
|
||||||
''' simple database lookup by username '''
|
|
||||||
result = outgoing.handle_remote_webfinger('@mouse@local.com')
|
|
||||||
self.assertEqual(result, self.local_user)
|
|
||||||
|
|
||||||
result = outgoing.handle_remote_webfinger('mouse@local.com')
|
|
||||||
self.assertEqual(result, self.local_user)
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_load_user(self):
|
|
||||||
''' find a remote user using webfinger '''
|
|
||||||
username = 'mouse@example.com'
|
|
||||||
wellknown = {
|
|
||||||
"subject": "acct:mouse@example.com",
|
|
||||||
"links": [{
|
|
||||||
"rel": "self",
|
|
||||||
"type": "application/activity+json",
|
|
||||||
"href": "https://example.com/user/mouse"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
responses.add(
|
|
||||||
responses.GET,
|
|
||||||
'https://example.com/.well-known/webfinger?resource=acct:%s' \
|
|
||||||
% username,
|
|
||||||
json=wellknown,
|
|
||||||
status=200)
|
|
||||||
responses.add(
|
|
||||||
responses.GET,
|
|
||||||
'https://example.com/user/mouse',
|
|
||||||
json=self.userdata,
|
|
||||||
status=200)
|
|
||||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
|
||||||
result = outgoing.handle_remote_webfinger('@mouse@example.com')
|
|
||||||
self.assertIsInstance(result, models.User)
|
|
||||||
self.assertEqual(result.username, 'mouse@example.com')
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_shelve(self):
|
|
||||||
''' shelve a book '''
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_shelve(self.local_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):
|
|
||||||
''' special behavior for the to-read shelf '''
|
|
||||||
shelf = models.Shelf.objects.get(identifier='to-read')
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_shelve(self.local_user, self.book, shelf)
|
|
||||||
# make sure the book is on the shelf
|
|
||||||
self.assertEqual(shelf.books.get(), self.book)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_shelve_reading(self):
|
|
||||||
''' special behavior for the reading shelf '''
|
|
||||||
shelf = models.Shelf.objects.get(identifier='reading')
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_shelve(self.local_user, self.book, shelf)
|
|
||||||
# make sure the book is on the shelf
|
|
||||||
self.assertEqual(shelf.books.get(), self.book)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_shelve_read(self):
|
|
||||||
''' special behavior for the read shelf '''
|
|
||||||
shelf = models.Shelf.objects.get(identifier='read')
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_shelve(self.local_user, self.book, shelf)
|
|
||||||
# make sure the book is on the shelf
|
|
||||||
self.assertEqual(shelf.books.get(), self.book)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_unshelve(self):
|
|
||||||
''' remove a book from a shelf '''
|
|
||||||
self.shelf.books.add(self.book)
|
|
||||||
self.shelf.save()
|
|
||||||
self.assertEqual(self.shelf.books.count(), 1)
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_unshelve(self.local_user, self.book, self.shelf)
|
|
||||||
self.assertEqual(self.shelf.books.count(), 0)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_reading_status_to_read(self):
|
|
||||||
''' posts shelve activities '''
|
|
||||||
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_reading_status(
|
|
||||||
self.local_user, shelf, self.book, 'public')
|
|
||||||
status = models.GeneratedNote.objects.get()
|
|
||||||
self.assertEqual(status.user, self.local_user)
|
|
||||||
self.assertEqual(status.mention_books.first(), self.book)
|
|
||||||
self.assertEqual(status.content, 'wants to read')
|
|
||||||
|
|
||||||
def test_handle_reading_status_reading(self):
|
|
||||||
''' posts shelve activities '''
|
|
||||||
shelf = self.local_user.shelf_set.get(identifier='reading')
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_reading_status(
|
|
||||||
self.local_user, shelf, self.book, 'public')
|
|
||||||
status = models.GeneratedNote.objects.get()
|
|
||||||
self.assertEqual(status.user, self.local_user)
|
|
||||||
self.assertEqual(status.mention_books.first(), self.book)
|
|
||||||
self.assertEqual(status.content, 'started reading')
|
|
||||||
|
|
||||||
def test_handle_reading_status_read(self):
|
|
||||||
''' posts shelve activities '''
|
|
||||||
shelf = self.local_user.shelf_set.get(identifier='read')
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_reading_status(
|
|
||||||
self.local_user, shelf, self.book, 'public')
|
|
||||||
status = models.GeneratedNote.objects.get()
|
|
||||||
self.assertEqual(status.user, self.local_user)
|
|
||||||
self.assertEqual(status.mention_books.first(), self.book)
|
|
||||||
self.assertEqual(status.content, 'finished reading')
|
|
||||||
|
|
||||||
def test_handle_reading_status_other(self):
|
|
||||||
''' posts shelve activities '''
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_reading_status(
|
|
||||||
self.local_user, self.shelf, self.book, 'public')
|
|
||||||
self.assertFalse(models.GeneratedNote.objects.exists())
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_imported_book(self):
|
|
||||||
''' goodreads import added a book, this adds related connections '''
|
|
||||||
shelf = self.local_user.shelf_set.filter(identifier='read').first()
|
|
||||||
self.assertIsNone(shelf.books.first())
|
|
||||||
|
|
||||||
import_job = models.ImportJob.objects.create(user=self.local_user)
|
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
|
||||||
csv_file = open(datafile, 'r')
|
|
||||||
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
|
||||||
import_item = models.ImportItem.objects.create(
|
|
||||||
job_id=import_job.id, index=index, data=entry, book=self.book)
|
|
||||||
break
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_imported_book(
|
|
||||||
self.local_user, import_item, False, 'public')
|
|
||||||
|
|
||||||
shelf.refresh_from_db()
|
|
||||||
self.assertEqual(shelf.books.first(), self.book)
|
|
||||||
|
|
||||||
readthrough = models.ReadThrough.objects.get(user=self.local_user)
|
|
||||||
self.assertEqual(readthrough.book, self.book)
|
|
||||||
# I can't remember how to create dates and I don't want to look it up.
|
|
||||||
self.assertEqual(readthrough.start_date.year, 2020)
|
|
||||||
self.assertEqual(readthrough.start_date.month, 10)
|
|
||||||
self.assertEqual(readthrough.start_date.day, 21)
|
|
||||||
self.assertEqual(readthrough.finish_date.year, 2020)
|
|
||||||
self.assertEqual(readthrough.finish_date.month, 10)
|
|
||||||
self.assertEqual(readthrough.finish_date.day, 25)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_imported_book_already_shelved(self):
|
|
||||||
''' goodreads import added a book, this adds related connections '''
|
|
||||||
shelf = self.local_user.shelf_set.filter(identifier='to-read').first()
|
|
||||||
models.ShelfBook.objects.create(
|
|
||||||
shelf=shelf, added_by=self.local_user, book=self.book)
|
|
||||||
|
|
||||||
import_job = models.ImportJob.objects.create(user=self.local_user)
|
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
|
||||||
csv_file = open(datafile, 'r')
|
|
||||||
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
|
||||||
import_item = models.ImportItem.objects.create(
|
|
||||||
job_id=import_job.id, index=index, data=entry, book=self.book)
|
|
||||||
break
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_imported_book(
|
|
||||||
self.local_user, import_item, False, 'public')
|
|
||||||
|
|
||||||
shelf.refresh_from_db()
|
|
||||||
self.assertEqual(shelf.books.first(), self.book)
|
|
||||||
self.assertIsNone(
|
|
||||||
self.local_user.shelf_set.get(identifier='read').books.first())
|
|
||||||
readthrough = models.ReadThrough.objects.get(user=self.local_user)
|
|
||||||
self.assertEqual(readthrough.book, self.book)
|
|
||||||
self.assertEqual(readthrough.start_date.year, 2020)
|
|
||||||
self.assertEqual(readthrough.start_date.month, 10)
|
|
||||||
self.assertEqual(readthrough.start_date.day, 21)
|
|
||||||
self.assertEqual(readthrough.finish_date.year, 2020)
|
|
||||||
self.assertEqual(readthrough.finish_date.month, 10)
|
|
||||||
self.assertEqual(readthrough.finish_date.day, 25)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_import_twice(self):
|
|
||||||
''' re-importing books '''
|
|
||||||
shelf = self.local_user.shelf_set.filter(identifier='read').first()
|
|
||||||
import_job = models.ImportJob.objects.create(user=self.local_user)
|
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
|
||||||
csv_file = open(datafile, 'r')
|
|
||||||
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
|
||||||
import_item = models.ImportItem.objects.create(
|
|
||||||
job_id=import_job.id, index=index, data=entry, book=self.book)
|
|
||||||
break
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_imported_book(
|
|
||||||
self.local_user, import_item, False, 'public')
|
|
||||||
outgoing.handle_imported_book(
|
|
||||||
self.local_user, import_item, False, 'public')
|
|
||||||
|
|
||||||
shelf.refresh_from_db()
|
|
||||||
self.assertEqual(shelf.books.first(), self.book)
|
|
||||||
|
|
||||||
readthrough = models.ReadThrough.objects.get(user=self.local_user)
|
|
||||||
self.assertEqual(readthrough.book, self.book)
|
|
||||||
# I can't remember how to create dates and I don't want to look it up.
|
|
||||||
self.assertEqual(readthrough.start_date.year, 2020)
|
|
||||||
self.assertEqual(readthrough.start_date.month, 10)
|
|
||||||
self.assertEqual(readthrough.start_date.day, 21)
|
|
||||||
self.assertEqual(readthrough.finish_date.year, 2020)
|
|
||||||
self.assertEqual(readthrough.finish_date.month, 10)
|
|
||||||
self.assertEqual(readthrough.finish_date.day, 25)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_imported_book_review(self):
|
|
||||||
''' goodreads review import '''
|
|
||||||
import_job = models.ImportJob.objects.create(user=self.local_user)
|
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
|
||||||
csv_file = open(datafile, 'r')
|
|
||||||
entry = list(csv.DictReader(csv_file))[2]
|
|
||||||
import_item = models.ImportItem.objects.create(
|
|
||||||
job_id=import_job.id, index=0, data=entry, book=self.book)
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_imported_book(
|
|
||||||
self.local_user, import_item, True, 'unlisted')
|
|
||||||
review = models.Review.objects.get(book=self.book, user=self.local_user)
|
|
||||||
self.assertEqual(review.content, 'mixed feelings')
|
|
||||||
self.assertEqual(review.rating, 2)
|
|
||||||
self.assertEqual(review.published_date.year, 2019)
|
|
||||||
self.assertEqual(review.published_date.month, 7)
|
|
||||||
self.assertEqual(review.published_date.day, 8)
|
|
||||||
self.assertEqual(review.privacy, 'unlisted')
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_imported_book_reviews_disabled(self):
|
|
||||||
''' goodreads review import '''
|
|
||||||
import_job = models.ImportJob.objects.create(user=self.local_user)
|
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
|
||||||
csv_file = open(datafile, 'r')
|
|
||||||
entry = list(csv.DictReader(csv_file))[2]
|
|
||||||
import_item = models.ImportItem.objects.create(
|
|
||||||
job_id=import_job.id, index=0, data=entry, book=self.book)
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_imported_book(
|
|
||||||
self.local_user, import_item, False, 'unlisted')
|
|
||||||
self.assertFalse(models.Review.objects.filter(
|
|
||||||
book=self.book, user=self.local_user
|
|
||||||
).exists())
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_delete_status(self):
|
|
||||||
''' marks a status as deleted '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
user=self.local_user, content='hi')
|
|
||||||
self.assertFalse(status.deleted)
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_delete_status(self.local_user, status)
|
|
||||||
status.refresh_from_db()
|
|
||||||
self.assertTrue(status.deleted)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_status(self):
|
|
||||||
''' create a status '''
|
|
||||||
form = forms.CommentForm({
|
|
||||||
'content': 'hi',
|
|
||||||
'user': self.local_user.id,
|
|
||||||
'book': self.book.id,
|
|
||||||
'privacy': 'public',
|
|
||||||
})
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_status(self.local_user, form)
|
|
||||||
status = models.Comment.objects.get()
|
|
||||||
self.assertEqual(status.content, '<p>hi</p>')
|
|
||||||
self.assertEqual(status.user, self.local_user)
|
|
||||||
self.assertEqual(status.book, self.book)
|
|
||||||
|
|
||||||
def test_handle_status_reply(self):
|
|
||||||
''' create a status in reply to an existing status '''
|
|
||||||
user = models.User.objects.create_user(
|
|
||||||
'rat', 'rat@rat.com', 'password', local=True)
|
|
||||||
parent = models.Status.objects.create(
|
|
||||||
content='parent status', user=self.local_user)
|
|
||||||
form = forms.ReplyForm({
|
|
||||||
'content': 'hi',
|
|
||||||
'user': user.id,
|
|
||||||
'reply_parent': parent.id,
|
|
||||||
'privacy': 'public',
|
|
||||||
})
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_status(user, form)
|
|
||||||
status = models.Status.objects.get(user=user)
|
|
||||||
self.assertEqual(status.content, '<p>hi</p>')
|
|
||||||
self.assertEqual(status.user, user)
|
|
||||||
self.assertEqual(
|
|
||||||
models.Notification.objects.get().user, self.local_user)
|
|
||||||
|
|
||||||
def test_handle_status_mentions(self):
|
|
||||||
''' @mention a user in a post '''
|
|
||||||
user = models.User.objects.create_user(
|
|
||||||
'rat@%s' % DOMAIN, 'rat@rat.com', 'password',
|
|
||||||
local=True, localname='rat')
|
|
||||||
form = forms.CommentForm({
|
|
||||||
'content': 'hi @rat',
|
|
||||||
'user': self.local_user.id,
|
|
||||||
'book': self.book.id,
|
|
||||||
'privacy': 'public',
|
|
||||||
})
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_status(self.local_user, form)
|
|
||||||
status = models.Status.objects.get()
|
|
||||||
self.assertEqual(list(status.mention_users.all()), [user])
|
|
||||||
self.assertEqual(models.Notification.objects.get().user, user)
|
|
||||||
self.assertEqual(
|
|
||||||
status.content,
|
|
||||||
'<p>hi <a href="%s">@rat</a></p>' % user.remote_id)
|
|
||||||
|
|
||||||
def test_handle_status_reply_with_mentions(self):
|
|
||||||
''' reply to a post with an @mention'ed user '''
|
|
||||||
user = models.User.objects.create_user(
|
|
||||||
'rat', 'rat@rat.com', 'password',
|
|
||||||
local=True, localname='rat')
|
|
||||||
form = forms.CommentForm({
|
|
||||||
'content': 'hi @rat@example.com',
|
|
||||||
'user': self.local_user.id,
|
|
||||||
'book': self.book.id,
|
|
||||||
'privacy': 'public',
|
|
||||||
})
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_status(self.local_user, form)
|
|
||||||
status = models.Status.objects.get()
|
|
||||||
|
|
||||||
form = forms.ReplyForm({
|
|
||||||
'content': 'right',
|
|
||||||
'user': user,
|
|
||||||
'privacy': 'public',
|
|
||||||
'reply_parent': status.id
|
|
||||||
})
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_status(user, form)
|
|
||||||
|
|
||||||
reply = models.Status.replies(status).first()
|
|
||||||
self.assertEqual(reply.content, '<p>right</p>')
|
|
||||||
self.assertEqual(reply.user, user)
|
|
||||||
self.assertTrue(self.remote_user in reply.mention_users.all())
|
|
||||||
self.assertTrue(self.local_user in reply.mention_users.all())
|
|
||||||
|
|
||||||
def test_find_mentions(self):
|
|
||||||
''' detect and look up @ mentions of users '''
|
|
||||||
user = models.User.objects.create_user(
|
|
||||||
'nutria@%s' % DOMAIN, 'nutria@nutria.com', 'password',
|
|
||||||
local=True, localname='nutria')
|
|
||||||
self.assertEqual(user.username, 'nutria@%s' % DOMAIN)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
list(outgoing.find_mentions('@nutria'))[0],
|
|
||||||
('@nutria', user)
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
list(outgoing.find_mentions('leading text @nutria'))[0],
|
|
||||||
('@nutria', user)
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
list(outgoing.find_mentions('leading @nutria trailing text'))[0],
|
|
||||||
('@nutria', user)
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
list(outgoing.find_mentions('@rat@example.com'))[0],
|
|
||||||
('@rat@example.com', self.remote_user)
|
|
||||||
)
|
|
||||||
|
|
||||||
multiple = list(outgoing.find_mentions('@nutria and @rat@example.com'))
|
|
||||||
self.assertEqual(multiple[0], ('@nutria', user))
|
|
||||||
self.assertEqual(multiple[1], ('@rat@example.com', self.remote_user))
|
|
||||||
|
|
||||||
with patch('bookwyrm.outgoing.handle_remote_webfinger') as rw:
|
|
||||||
rw.return_value = self.local_user
|
|
||||||
self.assertEqual(
|
|
||||||
list(outgoing.find_mentions('@beep@beep.com'))[0],
|
|
||||||
('@beep@beep.com', self.local_user)
|
|
||||||
)
|
|
||||||
with patch('bookwyrm.outgoing.handle_remote_webfinger') as rw:
|
|
||||||
rw.return_value = None
|
|
||||||
self.assertEqual(list(outgoing.find_mentions('@beep@beep.com')), [])
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
list(outgoing.find_mentions('@nutria@%s' % DOMAIN))[0],
|
|
||||||
('@nutria@%s' % DOMAIN, user)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_format_links(self):
|
|
||||||
''' find and format urls into a tags '''
|
|
||||||
url = 'http://www.fish.com/'
|
|
||||||
self.assertEqual(
|
|
||||||
outgoing.format_links(url),
|
|
||||||
'<a href="%s">www.fish.com/</a>' % url)
|
|
||||||
self.assertEqual(
|
|
||||||
outgoing.format_links('(%s)' % url),
|
|
||||||
'(<a href="%s">www.fish.com/</a>)' % url)
|
|
||||||
url = 'https://archive.org/details/dli.granth.72113/page/n25/mode/2up'
|
|
||||||
self.assertEqual(
|
|
||||||
outgoing.format_links(url),
|
|
||||||
'<a href="%s">' \
|
|
||||||
'archive.org/details/dli.granth.72113/page/n25/mode/2up</a>' \
|
|
||||||
% url)
|
|
||||||
url = 'https://openlibrary.org/search' \
|
|
||||||
'?q=arkady+strugatsky&mode=everything'
|
|
||||||
self.assertEqual(
|
|
||||||
outgoing.format_links(url),
|
|
||||||
'<a href="%s">openlibrary.org/search' \
|
|
||||||
'?q=arkady+strugatsky&mode=everything</a>' % url)
|
|
||||||
|
|
||||||
|
|
||||||
def test_to_markdown(self):
|
|
||||||
''' this is mostly handled in other places, but nonetheless '''
|
|
||||||
text = '_hi_ and http://fish.com is <marquee>rad</marquee>'
|
|
||||||
result = outgoing.to_markdown(text)
|
|
||||||
self.assertEqual(
|
|
||||||
result,
|
|
||||||
'<p><em>hi</em> and <a href="http://fish.com">fish.com</a> ' \
|
|
||||||
'is rad</p>')
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_favorite(self):
|
|
||||||
''' create and broadcast faving a status '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
user=self.local_user, content='hi')
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_favorite(self.remote_user, status)
|
|
||||||
fav = models.Favorite.objects.get()
|
|
||||||
self.assertEqual(fav.status, status)
|
|
||||||
self.assertEqual(fav.user, self.remote_user)
|
|
||||||
|
|
||||||
notification = models.Notification.objects.get()
|
|
||||||
self.assertEqual(notification.notification_type, 'FAVORITE')
|
|
||||||
self.assertEqual(notification.user, self.local_user)
|
|
||||||
self.assertEqual(notification.related_user, self.remote_user)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_unfavorite(self):
|
|
||||||
''' unfav a status '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
user=self.local_user, content='hi')
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_favorite(self.remote_user, status)
|
|
||||||
|
|
||||||
self.assertEqual(models.Favorite.objects.count(), 1)
|
|
||||||
self.assertEqual(models.Notification.objects.count(), 1)
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_unfavorite(self.remote_user, status)
|
|
||||||
self.assertEqual(models.Favorite.objects.count(), 0)
|
|
||||||
self.assertEqual(models.Notification.objects.count(), 0)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_boost(self):
|
|
||||||
''' boost a status '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
user=self.local_user, content='hi')
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_boost(self.remote_user, status)
|
|
||||||
|
|
||||||
boost = models.Boost.objects.get()
|
|
||||||
self.assertEqual(boost.boosted_status, status)
|
|
||||||
self.assertEqual(boost.user, self.remote_user)
|
|
||||||
self.assertEqual(boost.privacy, 'public')
|
|
||||||
|
|
||||||
notification = models.Notification.objects.get()
|
|
||||||
self.assertEqual(notification.notification_type, 'BOOST')
|
|
||||||
self.assertEqual(notification.user, self.local_user)
|
|
||||||
self.assertEqual(notification.related_user, self.remote_user)
|
|
||||||
self.assertEqual(notification.related_status, status)
|
|
||||||
|
|
||||||
def test_handle_boost_unlisted(self):
|
|
||||||
''' boost a status '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
user=self.local_user, content='hi', privacy='unlisted')
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_boost(self.remote_user, status)
|
|
||||||
|
|
||||||
boost = models.Boost.objects.get()
|
|
||||||
self.assertEqual(boost.privacy, 'unlisted')
|
|
||||||
|
|
||||||
def test_handle_boost_private(self):
|
|
||||||
''' boost a status '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
user=self.local_user, content='hi', privacy='followers')
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_boost(self.remote_user, status)
|
|
||||||
self.assertFalse(models.Boost.objects.exists())
|
|
||||||
|
|
||||||
def test_handle_boost_twice(self):
|
|
||||||
''' boost a status '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
user=self.local_user, content='hi')
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_boost(self.remote_user, status)
|
|
||||||
outgoing.handle_boost(self.remote_user, status)
|
|
||||||
self.assertEqual(models.Boost.objects.count(), 1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_unboost(self):
|
|
||||||
''' undo a boost '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
user=self.local_user, content='hi')
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_boost(self.remote_user, status)
|
|
||||||
|
|
||||||
self.assertEqual(models.Boost.objects.count(), 1)
|
|
||||||
self.assertEqual(models.Notification.objects.count(), 1)
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
outgoing.handle_unboost(self.remote_user, status)
|
|
||||||
self.assertEqual(models.Boost.objects.count(), 0)
|
|
||||||
self.assertEqual(models.Notification.objects.count(), 0)
|
|
|
@ -2,7 +2,6 @@
|
||||||
import re
|
import re
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from dateutil.parser import parse
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -213,3 +212,53 @@ class TemplateTags(TestCase):
|
||||||
r'[A-Z][a-z]{2} \d?\d \d{4}',
|
r'[A-Z][a-z]{2} \d?\d \d{4}',
|
||||||
bookwyrm_tags.time_since(years_ago)
|
bookwyrm_tags.time_since(years_ago)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_markdown(self):
|
||||||
|
''' mardown format data '''
|
||||||
|
result = bookwyrm_tags.get_markdown('_hi_')
|
||||||
|
self.assertEqual(result, '<p><em>hi</em></p>')
|
||||||
|
|
||||||
|
result = bookwyrm_tags.get_markdown('<marquee>_hi_</marquee>')
|
||||||
|
self.assertEqual(result, '<p><em>hi</em></p>')
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_mentions(self):
|
||||||
|
''' list of people mentioned '''
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
content='hi', user=self.remote_user)
|
||||||
|
result = bookwyrm_tags.get_mentions(status, self.user)
|
||||||
|
self.assertEqual(result, '@rat@example.com')
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_status_preview_name(self):
|
||||||
|
''' status context string '''
|
||||||
|
status = models.Status.objects.create(content='hi', user=self.user)
|
||||||
|
result = bookwyrm_tags.get_status_preview_name(status)
|
||||||
|
self.assertEqual(result, 'status')
|
||||||
|
|
||||||
|
status = models.Review.objects.create(
|
||||||
|
content='hi', user=self.user, book=self.book)
|
||||||
|
result = bookwyrm_tags.get_status_preview_name(status)
|
||||||
|
self.assertEqual(result, 'review of <em>Test Book</em>')
|
||||||
|
|
||||||
|
status = models.Comment.objects.create(
|
||||||
|
content='hi', user=self.user, book=self.book)
|
||||||
|
result = bookwyrm_tags.get_status_preview_name(status)
|
||||||
|
self.assertEqual(result, 'comment on <em>Test Book</em>')
|
||||||
|
|
||||||
|
status = models.Quotation.objects.create(
|
||||||
|
content='hi', user=self.user, book=self.book)
|
||||||
|
result = bookwyrm_tags.get_status_preview_name(status)
|
||||||
|
self.assertEqual(result, 'quotation from <em>Test Book</em>')
|
||||||
|
|
||||||
|
|
||||||
|
def test_related_status(self):
|
||||||
|
''' gets the subclass model for a notification status '''
|
||||||
|
status = models.Status.objects.create(content='hi', user=self.user)
|
||||||
|
notification = models.Notification.objects.create(
|
||||||
|
user=self.user, notification_type='MENTION',
|
||||||
|
related_status=status)
|
||||||
|
|
||||||
|
result = bookwyrm_tags.related_status(notification)
|
||||||
|
self.assertIsInstance(result, models.Status)
|
||||||
|
|
|
@ -1,534 +0,0 @@
|
||||||
''' test for app action functionality '''
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import dateutil
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.contrib.auth.models import Group, Permission
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.http.response import Http404
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from bookwyrm import forms, models, view_actions as actions
|
|
||||||
from bookwyrm.settings import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
#pylint: disable=too-many-public-methods
|
|
||||||
class ViewActions(TestCase):
|
|
||||||
''' a lot here: all handlers for receiving activitypub requests '''
|
|
||||||
def setUp(self):
|
|
||||||
''' we need basic things, like users '''
|
|
||||||
self.local_user = models.User.objects.create_user(
|
|
||||||
'mouse', 'mouse@mouse.com', 'mouseword',
|
|
||||||
local=True, localname='mouse')
|
|
||||||
self.local_user.remote_id = 'https://example.com/user/mouse'
|
|
||||||
self.local_user.save()
|
|
||||||
self.group = Group.objects.create(name='editor')
|
|
||||||
self.group.permissions.add(
|
|
||||||
Permission.objects.create(
|
|
||||||
name='edit_book',
|
|
||||||
codename='edit_book',
|
|
||||||
content_type=ContentType.objects.get_for_model(models.User)).id
|
|
||||||
)
|
|
||||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
|
||||||
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.status = models.Status.objects.create(
|
|
||||||
user=self.local_user,
|
|
||||||
content='Test status',
|
|
||||||
remote_id='https://example.com/status/1',
|
|
||||||
)
|
|
||||||
self.work = models.Work.objects.create(title='Test Work')
|
|
||||||
self.book = models.Edition.objects.create(
|
|
||||||
title='Test Book', parent_work=self.work)
|
|
||||||
self.settings = models.SiteSettings.objects.create(id=1)
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
|
|
||||||
|
|
||||||
def test_register(self):
|
|
||||||
''' create a user '''
|
|
||||||
self.assertEqual(models.User.objects.count(), 2)
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nutria-user.user_nutria',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.cccc'
|
|
||||||
})
|
|
||||||
with patch('bookwyrm.view_actions.login'):
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 3)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
nutria = models.User.objects.last()
|
|
||||||
self.assertEqual(nutria.username, 'nutria-user.user_nutria@%s' % DOMAIN)
|
|
||||||
self.assertEqual(nutria.localname, 'nutria-user.user_nutria')
|
|
||||||
self.assertEqual(nutria.local, True)
|
|
||||||
|
|
||||||
def test_register_trailing_space(self):
|
|
||||||
''' django handles this so weirdly '''
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nutria ',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.ccc'
|
|
||||||
})
|
|
||||||
with patch('bookwyrm.view_actions.login'):
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 3)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
nutria = models.User.objects.last()
|
|
||||||
self.assertEqual(nutria.username, 'nutria@%s' % DOMAIN)
|
|
||||||
self.assertEqual(nutria.localname, 'nutria')
|
|
||||||
self.assertEqual(nutria.local, True)
|
|
||||||
|
|
||||||
def test_register_invalid_email(self):
|
|
||||||
''' gotta have an email '''
|
|
||||||
self.assertEqual(models.User.objects.count(), 2)
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nutria',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa'
|
|
||||||
})
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 2)
|
|
||||||
self.assertEqual(response.template_name, 'login.html')
|
|
||||||
|
|
||||||
def test_register_invalid_username(self):
|
|
||||||
''' gotta have an email '''
|
|
||||||
self.assertEqual(models.User.objects.count(), 2)
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nut@ria',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.ccc'
|
|
||||||
})
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 2)
|
|
||||||
self.assertEqual(response.template_name, 'login.html')
|
|
||||||
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nutr ia',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.ccc'
|
|
||||||
})
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 2)
|
|
||||||
self.assertEqual(response.template_name, 'login.html')
|
|
||||||
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nut@ria',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.ccc'
|
|
||||||
})
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 2)
|
|
||||||
self.assertEqual(response.template_name, 'login.html')
|
|
||||||
|
|
||||||
|
|
||||||
def test_register_closed_instance(self):
|
|
||||||
''' you can't just register '''
|
|
||||||
self.settings.allow_registration = False
|
|
||||||
self.settings.save()
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nutria ',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.ccc'
|
|
||||||
})
|
|
||||||
with self.assertRaises(PermissionDenied):
|
|
||||||
actions.register(request)
|
|
||||||
|
|
||||||
def test_register_invite(self):
|
|
||||||
''' you can't just register '''
|
|
||||||
self.settings.allow_registration = False
|
|
||||||
self.settings.save()
|
|
||||||
models.SiteInvite.objects.create(
|
|
||||||
code='testcode', user=self.local_user, use_limit=1)
|
|
||||||
self.assertEqual(models.SiteInvite.objects.get().times_used, 0)
|
|
||||||
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nutria',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.ccc',
|
|
||||||
'invite_code': 'testcode'
|
|
||||||
})
|
|
||||||
with patch('bookwyrm.view_actions.login'):
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 3)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(models.SiteInvite.objects.get().times_used, 1)
|
|
||||||
|
|
||||||
# invite already used to max capacity
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nutria2',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.ccc',
|
|
||||||
'invite_code': 'testcode'
|
|
||||||
})
|
|
||||||
with self.assertRaises(PermissionDenied):
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 3)
|
|
||||||
|
|
||||||
# bad invite code
|
|
||||||
request = self.factory.post(
|
|
||||||
'register/',
|
|
||||||
{
|
|
||||||
'localname': 'nutria3',
|
|
||||||
'password': 'mouseword',
|
|
||||||
'email': 'aa@bb.ccc',
|
|
||||||
'invite_code': 'dkfkdjgdfkjgkdfj'
|
|
||||||
})
|
|
||||||
with self.assertRaises(Http404):
|
|
||||||
response = actions.register(request)
|
|
||||||
self.assertEqual(models.User.objects.count(), 3)
|
|
||||||
|
|
||||||
|
|
||||||
def test_password_reset_request(self):
|
|
||||||
''' send 'em an email '''
|
|
||||||
request = self.factory.post('', {'email': 'aa@bb.ccc'})
|
|
||||||
resp = actions.password_reset_request(request)
|
|
||||||
self.assertEqual(resp.status_code, 302)
|
|
||||||
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {'email': 'mouse@mouse.com'})
|
|
||||||
with patch('bookwyrm.emailing.send_email.delay'):
|
|
||||||
resp = actions.password_reset_request(request)
|
|
||||||
self.assertEqual(resp.template_name, 'password_reset_request.html')
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
models.PasswordReset.objects.get().user, self.local_user)
|
|
||||||
|
|
||||||
def test_password_reset(self):
|
|
||||||
''' reset from code '''
|
|
||||||
code = models.PasswordReset.objects.create(user=self.local_user)
|
|
||||||
request = self.factory.post('', {
|
|
||||||
'reset-code': code.code,
|
|
||||||
'password': 'hi',
|
|
||||||
'confirm-password': 'hi'
|
|
||||||
})
|
|
||||||
with patch('bookwyrm.view_actions.login'):
|
|
||||||
resp = actions.password_reset(request)
|
|
||||||
self.assertEqual(resp.status_code, 302)
|
|
||||||
self.assertFalse(models.PasswordReset.objects.exists())
|
|
||||||
|
|
||||||
def test_password_reset_wrong_code(self):
|
|
||||||
''' reset from code '''
|
|
||||||
models.PasswordReset.objects.create(user=self.local_user)
|
|
||||||
request = self.factory.post('', {
|
|
||||||
'reset-code': 'jhgdkfjgdf',
|
|
||||||
'password': 'hi',
|
|
||||||
'confirm-password': 'hi'
|
|
||||||
})
|
|
||||||
resp = actions.password_reset(request)
|
|
||||||
self.assertEqual(resp.template_name, 'password_reset.html')
|
|
||||||
self.assertTrue(models.PasswordReset.objects.exists())
|
|
||||||
|
|
||||||
def test_password_reset_mismatch(self):
|
|
||||||
''' reset from code '''
|
|
||||||
code = models.PasswordReset.objects.create(user=self.local_user)
|
|
||||||
request = self.factory.post('', {
|
|
||||||
'reset-code': code.code,
|
|
||||||
'password': 'hi',
|
|
||||||
'confirm-password': 'hihi'
|
|
||||||
})
|
|
||||||
resp = actions.password_reset(request)
|
|
||||||
self.assertEqual(resp.template_name, 'password_reset.html')
|
|
||||||
self.assertTrue(models.PasswordReset.objects.exists())
|
|
||||||
|
|
||||||
|
|
||||||
def test_password_change(self):
|
|
||||||
''' change password '''
|
|
||||||
password_hash = self.local_user.password
|
|
||||||
request = self.factory.post('', {
|
|
||||||
'password': 'hi',
|
|
||||||
'confirm-password': 'hi'
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.view_actions.login'):
|
|
||||||
actions.password_change(request)
|
|
||||||
self.assertNotEqual(self.local_user.password, password_hash)
|
|
||||||
|
|
||||||
def test_password_change_mismatch(self):
|
|
||||||
''' change password '''
|
|
||||||
password_hash = self.local_user.password
|
|
||||||
request = self.factory.post('', {
|
|
||||||
'password': 'hi',
|
|
||||||
'confirm-password': 'hihi'
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
actions.password_change(request)
|
|
||||||
self.assertEqual(self.local_user.password, password_hash)
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_user(self):
|
|
||||||
''' use a form to update a user '''
|
|
||||||
form = forms.EditUserForm(instance=self.local_user)
|
|
||||||
form.data['name'] = 'New Name'
|
|
||||||
request = self.factory.post('', form.data)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
actions.edit_profile(request)
|
|
||||||
self.assertEqual(self.local_user.name, 'New Name')
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_book(self):
|
|
||||||
''' lets a user edit a book '''
|
|
||||||
self.local_user.groups.add(self.group)
|
|
||||||
form = forms.EditionForm(instance=self.book)
|
|
||||||
form.data['title'] = 'New Title'
|
|
||||||
form.data['last_edited_by'] = self.local_user.id
|
|
||||||
request = self.factory.post('', form.data)
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
actions.edit_book(request, self.book.id)
|
|
||||||
self.book.refresh_from_db()
|
|
||||||
self.assertEqual(self.book.title, 'New Title')
|
|
||||||
|
|
||||||
|
|
||||||
def test_switch_edition(self):
|
|
||||||
''' updates user's relationships to a book '''
|
|
||||||
work = models.Work.objects.create(title='test work')
|
|
||||||
edition1 = models.Edition.objects.create(
|
|
||||||
title='first ed', parent_work=work)
|
|
||||||
edition2 = models.Edition.objects.create(
|
|
||||||
title='second ed', parent_work=work)
|
|
||||||
shelf = models.Shelf.objects.create(
|
|
||||||
name='Test Shelf', user=self.local_user)
|
|
||||||
shelf.books.add(edition1)
|
|
||||||
models.ReadThrough.objects.create(
|
|
||||||
user=self.local_user, book=edition1)
|
|
||||||
|
|
||||||
self.assertEqual(models.ShelfBook.objects.get().book, edition1)
|
|
||||||
self.assertEqual(models.ReadThrough.objects.get().book, edition1)
|
|
||||||
request = self.factory.post('', {
|
|
||||||
'edition': edition2.id
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
actions.switch_edition(request)
|
|
||||||
|
|
||||||
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
|
|
||||||
self.assertEqual(models.ReadThrough.objects.get().book, edition2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_author(self):
|
|
||||||
''' edit an author '''
|
|
||||||
author = models.Author.objects.create(name='Test Author')
|
|
||||||
self.local_user.groups.add(self.group)
|
|
||||||
form = forms.AuthorForm(instance=author)
|
|
||||||
form.data['name'] = 'New Name'
|
|
||||||
form.data['last_edited_by'] = self.local_user.id
|
|
||||||
request = self.factory.post('', form.data)
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
actions.edit_author(request, author.id)
|
|
||||||
author.refresh_from_db()
|
|
||||||
self.assertEqual(author.name, 'New Name')
|
|
||||||
self.assertEqual(author.last_edited_by, self.local_user)
|
|
||||||
|
|
||||||
def test_edit_author_non_editor(self):
|
|
||||||
''' edit an author with invalid post data'''
|
|
||||||
author = models.Author.objects.create(name='Test Author')
|
|
||||||
form = forms.AuthorForm(instance=author)
|
|
||||||
form.data['name'] = 'New Name'
|
|
||||||
form.data['last_edited_by'] = self.local_user.id
|
|
||||||
request = self.factory.post('', form.data)
|
|
||||||
request.user = self.local_user
|
|
||||||
with self.assertRaises(PermissionDenied):
|
|
||||||
actions.edit_author(request, author.id)
|
|
||||||
author.refresh_from_db()
|
|
||||||
self.assertEqual(author.name, 'Test Author')
|
|
||||||
|
|
||||||
def test_edit_author_invalid_form(self):
|
|
||||||
''' edit an author with invalid post data'''
|
|
||||||
author = models.Author.objects.create(name='Test Author')
|
|
||||||
self.local_user.groups.add(self.group)
|
|
||||||
form = forms.AuthorForm(instance=author)
|
|
||||||
form.data['name'] = ''
|
|
||||||
form.data['last_edited_by'] = self.local_user.id
|
|
||||||
request = self.factory.post('', form.data)
|
|
||||||
request.user = self.local_user
|
|
||||||
resp = actions.edit_author(request, author.id)
|
|
||||||
author.refresh_from_db()
|
|
||||||
self.assertEqual(author.name, 'Test Author')
|
|
||||||
self.assertEqual(resp.template_name, 'edit_author.html')
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_shelf_privacy(self):
|
|
||||||
''' set name or privacy on shelf '''
|
|
||||||
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
|
||||||
self.assertEqual(shelf.privacy, 'public')
|
|
||||||
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {
|
|
||||||
'privacy': 'unlisted',
|
|
||||||
'user': self.local_user.id,
|
|
||||||
'name': 'To Read',
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
actions.edit_shelf(request, shelf.id)
|
|
||||||
shelf.refresh_from_db()
|
|
||||||
|
|
||||||
self.assertEqual(shelf.privacy, 'unlisted')
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_shelf_name(self):
|
|
||||||
''' change the name of an editable shelf '''
|
|
||||||
shelf = models.Shelf.objects.create(
|
|
||||||
name='Test Shelf', user=self.local_user)
|
|
||||||
self.assertEqual(shelf.privacy, 'public')
|
|
||||||
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {
|
|
||||||
'privacy': 'public',
|
|
||||||
'user': self.local_user.id,
|
|
||||||
'name': 'cool name'
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
actions.edit_shelf(request, shelf.id)
|
|
||||||
shelf.refresh_from_db()
|
|
||||||
|
|
||||||
self.assertEqual(shelf.name, 'cool name')
|
|
||||||
self.assertEqual(shelf.identifier, 'testshelf-%d' % shelf.id)
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_shelf_name_not_editable(self):
|
|
||||||
''' can't change the name of an non-editable shelf '''
|
|
||||||
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
|
||||||
self.assertEqual(shelf.privacy, 'public')
|
|
||||||
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {
|
|
||||||
'privacy': 'public',
|
|
||||||
'user': self.local_user.id,
|
|
||||||
'name': 'cool name'
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
actions.edit_shelf(request, shelf.id)
|
|
||||||
|
|
||||||
self.assertEqual(shelf.name, 'To Read')
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_readthrough(self):
|
|
||||||
''' adding dates to an ongoing readthrough '''
|
|
||||||
start = timezone.make_aware(dateutil.parser.parse('2021-01-03'))
|
|
||||||
readthrough = models.ReadThrough.objects.create(
|
|
||||||
book=self.book, user=self.local_user, start_date=start)
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {
|
|
||||||
'start_date': '2017-01-01',
|
|
||||||
'finish_date': '2018-03-07',
|
|
||||||
'book': '',
|
|
||||||
'id': readthrough.id,
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
actions.edit_readthrough(request)
|
|
||||||
readthrough.refresh_from_db()
|
|
||||||
self.assertEqual(readthrough.start_date.year, 2017)
|
|
||||||
self.assertEqual(readthrough.start_date.month, 1)
|
|
||||||
self.assertEqual(readthrough.start_date.day, 1)
|
|
||||||
self.assertEqual(readthrough.finish_date.year, 2018)
|
|
||||||
self.assertEqual(readthrough.finish_date.month, 3)
|
|
||||||
self.assertEqual(readthrough.finish_date.day, 7)
|
|
||||||
self.assertEqual(readthrough.book, self.book)
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_readthrough(self):
|
|
||||||
''' remove a readthrough '''
|
|
||||||
readthrough = models.ReadThrough.objects.create(
|
|
||||||
book=self.book, user=self.local_user)
|
|
||||||
models.ReadThrough.objects.create(
|
|
||||||
book=self.book, user=self.local_user)
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {
|
|
||||||
'id': readthrough.id,
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
actions.delete_readthrough(request)
|
|
||||||
self.assertFalse(
|
|
||||||
models.ReadThrough.objects.filter(id=readthrough.id).exists())
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_readthrough(self):
|
|
||||||
''' adding new read dates '''
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {
|
|
||||||
'start_date': '2017-01-01',
|
|
||||||
'finish_date': '2018-03-07',
|
|
||||||
'book': self.book.id,
|
|
||||||
'id': '',
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
actions.create_readthrough(request)
|
|
||||||
readthrough = models.ReadThrough.objects.get()
|
|
||||||
self.assertEqual(readthrough.start_date.year, 2017)
|
|
||||||
self.assertEqual(readthrough.start_date.month, 1)
|
|
||||||
self.assertEqual(readthrough.start_date.day, 1)
|
|
||||||
self.assertEqual(readthrough.finish_date.year, 2018)
|
|
||||||
self.assertEqual(readthrough.finish_date.month, 3)
|
|
||||||
self.assertEqual(readthrough.finish_date.day, 7)
|
|
||||||
self.assertEqual(readthrough.book, self.book)
|
|
||||||
self.assertEqual(readthrough.user, self.local_user)
|
|
||||||
|
|
||||||
|
|
||||||
def test_tag(self):
|
|
||||||
''' add a tag to a book '''
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {
|
|
||||||
'name': 'A Tag!?',
|
|
||||||
'book': self.book.id,
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
actions.tag(request)
|
|
||||||
|
|
||||||
tag = models.Tag.objects.get()
|
|
||||||
user_tag = models.UserTag.objects.get()
|
|
||||||
self.assertEqual(tag.name, 'A Tag!?')
|
|
||||||
self.assertEqual(tag.identifier, 'A+Tag%21%3F')
|
|
||||||
self.assertEqual(user_tag.user, self.local_user)
|
|
||||||
self.assertEqual(user_tag.book, self.book)
|
|
||||||
|
|
||||||
|
|
||||||
def test_untag(self):
|
|
||||||
''' remove a tag from a book '''
|
|
||||||
tag = models.Tag.objects.create(name='A Tag!?')
|
|
||||||
models.UserTag.objects.create(
|
|
||||||
user=self.local_user, book=self.book, tag=tag)
|
|
||||||
request = self.factory.post(
|
|
||||||
'', {
|
|
||||||
'user': self.local_user.id,
|
|
||||||
'book': self.book.id,
|
|
||||||
'name': tag.name,
|
|
||||||
})
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
actions.untag(request)
|
|
||||||
|
|
||||||
self.assertTrue(models.Tag.objects.filter(name='A Tag!?').exists())
|
|
||||||
self.assertFalse(models.UserTag.objects.exists())
|
|
|
@ -1,597 +0,0 @@
|
||||||
''' test for app action functionality '''
|
|
||||||
import json
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.template.response import TemplateResponse
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
|
|
||||||
from bookwyrm import models, views
|
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
|
||||||
from bookwyrm.connectors import abstract_connector
|
|
||||||
from bookwyrm.settings import DOMAIN, USER_AGENT
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
|
||||||
class Views(TestCase):
|
|
||||||
''' every response to a get request, html or json '''
|
|
||||||
def setUp(self):
|
|
||||||
''' we need basic test data and mocks '''
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
self.work = models.Work.objects.create(title='Test Work')
|
|
||||||
self.book = models.Edition.objects.create(
|
|
||||||
title='Test Book', parent_work=self.work)
|
|
||||||
models.Connector.objects.create(
|
|
||||||
identifier='self',
|
|
||||||
connector_file='self_connector',
|
|
||||||
local=True
|
|
||||||
)
|
|
||||||
self.local_user = models.User.objects.create_user(
|
|
||||||
'mouse@local.com', 'mouse@mouse.mouse', 'password',
|
|
||||||
local=True, localname='mouse')
|
|
||||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
|
||||||
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',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_edition(self):
|
|
||||||
''' given an edition or a work, returns an edition '''
|
|
||||||
self.assertEqual(
|
|
||||||
views.get_edition(self.book.id), self.book)
|
|
||||||
self.assertEqual(
|
|
||||||
views.get_edition(self.work.id), self.book)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_user_from_username(self):
|
|
||||||
''' works for either localname or username '''
|
|
||||||
self.assertEqual(
|
|
||||||
views.get_user_from_username('mouse'), self.local_user)
|
|
||||||
self.assertEqual(
|
|
||||||
views.get_user_from_username('mouse@local.com'), self.local_user)
|
|
||||||
with self.assertRaises(models.User.DoesNotExist):
|
|
||||||
views.get_user_from_username('mojfse@example.com')
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_api_request(self):
|
|
||||||
''' should it return html or json '''
|
|
||||||
request = self.factory.get('/path')
|
|
||||||
request.headers = {'Accept': 'application/json'}
|
|
||||||
self.assertTrue(views.is_api_request(request))
|
|
||||||
|
|
||||||
request = self.factory.get('/path.json')
|
|
||||||
request.headers = {'Accept': 'Praise'}
|
|
||||||
self.assertTrue(views.is_api_request(request))
|
|
||||||
|
|
||||||
request = self.factory.get('/path')
|
|
||||||
request.headers = {'Accept': 'Praise'}
|
|
||||||
self.assertFalse(views.is_api_request(request))
|
|
||||||
|
|
||||||
|
|
||||||
def test_home_tab(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.home_tab(request, 'local')
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'feed.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_direct_messages_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.direct_messages_page(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'direct_messages.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_activity_feed(self):
|
|
||||||
''' loads statuses '''
|
|
||||||
rat = models.User.objects.create_user(
|
|
||||||
'rat', 'rat@rat.rat', 'password', local=True)
|
|
||||||
|
|
||||||
public_status = models.Comment.objects.create(
|
|
||||||
content='public status', book=self.book, user=self.local_user)
|
|
||||||
direct_status = models.Status.objects.create(
|
|
||||||
content='direct', user=self.local_user, privacy='direct')
|
|
||||||
|
|
||||||
rat_public = models.Status.objects.create(
|
|
||||||
content='blah blah', user=rat)
|
|
||||||
rat_unlisted = models.Status.objects.create(
|
|
||||||
content='blah blah', user=rat, privacy='unlisted')
|
|
||||||
remote_status = models.Status.objects.create(
|
|
||||||
content='blah blah', user=self.remote_user)
|
|
||||||
followers_status = models.Status.objects.create(
|
|
||||||
content='blah', user=rat, privacy='followers')
|
|
||||||
rat_mention = models.Status.objects.create(
|
|
||||||
content='blah blah blah', user=rat, privacy='followers')
|
|
||||||
rat_mention.mention_users.set([self.local_user])
|
|
||||||
|
|
||||||
statuses = views.get_activity_feed(
|
|
||||||
self.local_user,
|
|
||||||
['public', 'unlisted', 'followers'],
|
|
||||||
following_only=True,
|
|
||||||
queryset=models.Comment.objects
|
|
||||||
)
|
|
||||||
self.assertEqual(len(statuses), 1)
|
|
||||||
self.assertEqual(statuses[0], public_status)
|
|
||||||
|
|
||||||
statuses = views.get_activity_feed(
|
|
||||||
self.local_user,
|
|
||||||
['public', 'followers'],
|
|
||||||
local_only=True
|
|
||||||
)
|
|
||||||
self.assertEqual(len(statuses), 2)
|
|
||||||
self.assertEqual(statuses[1], public_status)
|
|
||||||
self.assertEqual(statuses[0], rat_public)
|
|
||||||
|
|
||||||
statuses = views.get_activity_feed(self.local_user, 'direct')
|
|
||||||
self.assertEqual(len(statuses), 1)
|
|
||||||
self.assertEqual(statuses[0], direct_status)
|
|
||||||
|
|
||||||
statuses = views.get_activity_feed(
|
|
||||||
self.local_user,
|
|
||||||
['public', 'followers'],
|
|
||||||
)
|
|
||||||
self.assertEqual(len(statuses), 3)
|
|
||||||
self.assertEqual(statuses[2], public_status)
|
|
||||||
self.assertEqual(statuses[1], rat_public)
|
|
||||||
self.assertEqual(statuses[0], remote_status)
|
|
||||||
|
|
||||||
statuses = views.get_activity_feed(
|
|
||||||
self.local_user,
|
|
||||||
['public', 'unlisted', 'followers'],
|
|
||||||
following_only=True
|
|
||||||
)
|
|
||||||
self.assertEqual(len(statuses), 2)
|
|
||||||
self.assertEqual(statuses[1], public_status)
|
|
||||||
self.assertEqual(statuses[0], rat_mention)
|
|
||||||
|
|
||||||
rat.followers.add(self.local_user)
|
|
||||||
statuses = views.get_activity_feed(
|
|
||||||
self.local_user,
|
|
||||||
['public', 'unlisted', 'followers'],
|
|
||||||
following_only=True
|
|
||||||
)
|
|
||||||
self.assertEqual(len(statuses), 5)
|
|
||||||
self.assertEqual(statuses[4], public_status)
|
|
||||||
self.assertEqual(statuses[3], rat_public)
|
|
||||||
self.assertEqual(statuses[2], rat_unlisted)
|
|
||||||
self.assertEqual(statuses[1], followers_status)
|
|
||||||
self.assertEqual(statuses[0], rat_mention)
|
|
||||||
|
|
||||||
|
|
||||||
def test_search_json_response(self):
|
|
||||||
''' searches local data only and returns book data in json format '''
|
|
||||||
# we need a connector for this, sorry
|
|
||||||
request = self.factory.get('', {'q': 'Test Book'})
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
response = views.search(request)
|
|
||||||
self.assertIsInstance(response, JsonResponse)
|
|
||||||
|
|
||||||
data = json.loads(response.content)
|
|
||||||
self.assertEqual(len(data), 1)
|
|
||||||
self.assertEqual(data[0]['title'], 'Test Book')
|
|
||||||
self.assertEqual(
|
|
||||||
data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id))
|
|
||||||
|
|
||||||
|
|
||||||
def test_search_html_response(self):
|
|
||||||
''' searches remote connectors '''
|
|
||||||
class TestConnector(abstract_connector.AbstractMinimalConnector):
|
|
||||||
''' nothing added here '''
|
|
||||||
def format_search_result(self, search_result):
|
|
||||||
pass
|
|
||||||
def get_or_create_book(self, remote_id):
|
|
||||||
pass
|
|
||||||
def parse_search_data(self, data):
|
|
||||||
pass
|
|
||||||
models.Connector.objects.create(
|
|
||||||
identifier='example.com',
|
|
||||||
connector_file='openlibrary',
|
|
||||||
base_url='https://example.com',
|
|
||||||
books_url='https://example.com/books',
|
|
||||||
covers_url='https://example.com/covers',
|
|
||||||
search_url='https://example.com/search?q=',
|
|
||||||
)
|
|
||||||
connector = TestConnector('example.com')
|
|
||||||
|
|
||||||
search_result = abstract_connector.SearchResult(
|
|
||||||
key='http://www.example.com/book/1',
|
|
||||||
title='Gideon the Ninth',
|
|
||||||
author='Tamsyn Muir',
|
|
||||||
year='2019',
|
|
||||||
connector=connector
|
|
||||||
)
|
|
||||||
|
|
||||||
request = self.factory.get('', {'q': 'Test Book'})
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
with patch(
|
|
||||||
'bookwyrm.connectors.connector_manager.search') as manager:
|
|
||||||
manager.return_value = [search_result]
|
|
||||||
response = views.search(request)
|
|
||||||
self.assertIsInstance(response, TemplateResponse)
|
|
||||||
self.assertEqual(response.template_name, 'search_results.html')
|
|
||||||
self.assertEqual(
|
|
||||||
response.context_data['book_results'][0].title, 'Gideon the Ninth')
|
|
||||||
|
|
||||||
|
|
||||||
def test_search_html_response_users(self):
|
|
||||||
''' searches remote connectors '''
|
|
||||||
request = self.factory.get('', {'q': 'mouse'})
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
with patch('bookwyrm.connectors.connector_manager.search'):
|
|
||||||
response = views.search(request)
|
|
||||||
self.assertIsInstance(response, TemplateResponse)
|
|
||||||
self.assertEqual(response.template_name, 'search_results.html')
|
|
||||||
self.assertEqual(
|
|
||||||
response.context_data['user_results'][0], self.local_user)
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.import_page(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'import.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_status(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
import_job = models.ImportJob.objects.create(user=self.local_user)
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.tasks.app.AsyncResult') as async_result:
|
|
||||||
async_result.return_value = []
|
|
||||||
result = views.import_status(request, import_job.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'import_status.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_login_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = AnonymousUser
|
|
||||||
result = views.login_page(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'login.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.login_page(request)
|
|
||||||
self.assertEqual(result.url, '/')
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
|
|
||||||
|
|
||||||
def test_about_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.about_page(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'about.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_password_reset_request(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.password_reset_request(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'password_reset_request.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_password_reset(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
code = models.PasswordReset.objects.create(user=self.local_user)
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = AnonymousUser
|
|
||||||
result = views.password_reset(request, code.code)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'password_reset.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_invite_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
models.SiteInvite.objects.create(code='hi', user=self.local_user)
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = AnonymousUser
|
|
||||||
# why?? this is annoying.
|
|
||||||
request.user.is_authenticated = False
|
|
||||||
with patch('bookwyrm.models.site.SiteInvite.valid') as invite:
|
|
||||||
invite.return_value = True
|
|
||||||
result = views.invite_page(request, 'hi')
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'invite.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_manage_invites(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
request.user.is_superuser = True
|
|
||||||
result = views.manage_invites(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'manage_invites.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_notifications_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.notifications_page(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'notifications.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.user_page(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'user.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.user_page(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_followers_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.followers_page(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'followers.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.followers_page(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_following_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.following_page(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'following.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.following_page(request, 'mouse')
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_status_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
content='hi', user=self.local_user)
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.status_page(request, 'mouse', status.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'status.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.status_page(request, 'mouse', status.id)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_replies_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
content='hi', user=self.local_user)
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.replies_page(request, 'mouse', status.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'status.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.replies_page(request, 'mouse', status.id)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_profile_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.edit_profile_page(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'edit_user.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_book_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.book_page(request, self.book.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'book.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.book_page(request, self.book.id)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_book_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
request.user.is_superuser = True
|
|
||||||
result = views.edit_book_page(request, self.book.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'edit_book.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_author_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
author = models.Author.objects.create(name='Test Author')
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
request.user.is_superuser = True
|
|
||||||
result = views.edit_author_page(request, author.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'edit_author.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_editions_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.editions_page(request, self.work.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'editions.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.editions_page(request, self.work.id)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_author_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
author = models.Author.objects.create(name='Jessica')
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.author_page(request, author.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'author.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.author_page(request, author.id)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_tag_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
tag = models.Tag.objects.create(name='hi there')
|
|
||||||
models.UserTag.objects.create(
|
|
||||||
tag=tag, user=self.local_user, book=self.book)
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.tag_page(request, tag.identifier)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'tag.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.tag_page(request, tag.identifier)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_shelf_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
shelf = self.local_user.shelf_set.first()
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.shelf_page(
|
|
||||||
request, self.local_user.username, shelf.identifier)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'shelf.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.shelf_page(
|
|
||||||
request, self.local_user.username, shelf.identifier)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
request = self.factory.get('/?page=1')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.shelf_page(
|
|
||||||
request, self.local_user.username, shelf.identifier)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_bookwyrm_request(self):
|
|
||||||
''' checks if a request came from a bookwyrm instance '''
|
|
||||||
request = self.factory.get('', {'q': 'Test Book'})
|
|
||||||
self.assertFalse(views.is_bookworm_request(request))
|
|
||||||
|
|
||||||
request = self.factory.get(
|
|
||||||
'', {'q': 'Test Book'},
|
|
||||||
HTTP_USER_AGENT=\
|
|
||||||
"http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)"
|
|
||||||
)
|
|
||||||
self.assertFalse(views.is_bookworm_request(request))
|
|
||||||
|
|
||||||
request = self.factory.get(
|
|
||||||
'', {'q': 'Test Book'}, HTTP_USER_AGENT=USER_AGENT)
|
|
||||||
self.assertTrue(views.is_bookworm_request(request))
|
|
302
bookwyrm/tests/views/test_authentication.py
Normal file
302
bookwyrm/tests/views/test_authentication.py
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http.response import Http404
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
|
class AuthenticationViews(TestCase):
|
||||||
|
''' login and password management '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'password',
|
||||||
|
local=True, localname='mouse')
|
||||||
|
self.anonymous_user = AnonymousUser
|
||||||
|
self.anonymous_user.is_authenticated = False
|
||||||
|
self.settings = models.SiteSettings.objects.create(id=1)
|
||||||
|
|
||||||
|
def test_login_get(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
login = views.Login.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.anonymous_user
|
||||||
|
|
||||||
|
result = login(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'login.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request.user = self.local_user
|
||||||
|
result = login(request)
|
||||||
|
self.assertEqual(result.url, '/')
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_reset_request(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.PasswordResetRequest.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'password_reset_request.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_reset_request_post(self):
|
||||||
|
''' send 'em an email '''
|
||||||
|
request = self.factory.post('', {'email': 'aa@bb.ccc'})
|
||||||
|
view = views.PasswordResetRequest.as_view()
|
||||||
|
resp = view(request)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
request = self.factory.post('', {'email': 'mouse@mouse.com'})
|
||||||
|
with patch('bookwyrm.emailing.send_email.delay'):
|
||||||
|
resp = view(request)
|
||||||
|
self.assertEqual(resp.template_name, 'password_reset_request.html')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
models.PasswordReset.objects.get().user, self.local_user)
|
||||||
|
|
||||||
|
def test_password_reset(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.PasswordReset.as_view()
|
||||||
|
code = models.PasswordReset.objects.create(user=self.local_user)
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.anonymous_user
|
||||||
|
result = view(request, code.code)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'password_reset.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_reset_post(self):
|
||||||
|
''' reset from code '''
|
||||||
|
view = views.PasswordReset.as_view()
|
||||||
|
code = models.PasswordReset.objects.create(user=self.local_user)
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'password': 'hi',
|
||||||
|
'confirm-password': 'hi'
|
||||||
|
})
|
||||||
|
with patch('bookwyrm.views.password.login'):
|
||||||
|
resp = view(request, code.code)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertFalse(models.PasswordReset.objects.exists())
|
||||||
|
|
||||||
|
def test_password_reset_wrong_code(self):
|
||||||
|
''' reset from code '''
|
||||||
|
view = views.PasswordReset.as_view()
|
||||||
|
models.PasswordReset.objects.create(user=self.local_user)
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'password': 'hi',
|
||||||
|
'confirm-password': 'hi'
|
||||||
|
})
|
||||||
|
resp = view(request, 'jhgdkfjgdf')
|
||||||
|
self.assertEqual(resp.template_name, 'password_reset.html')
|
||||||
|
self.assertTrue(models.PasswordReset.objects.exists())
|
||||||
|
|
||||||
|
def test_password_reset_mismatch(self):
|
||||||
|
''' reset from code '''
|
||||||
|
view = views.PasswordReset.as_view()
|
||||||
|
code = models.PasswordReset.objects.create(user=self.local_user)
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'password': 'hi',
|
||||||
|
'confirm-password': 'hihi'
|
||||||
|
})
|
||||||
|
resp = view(request, code.code)
|
||||||
|
self.assertEqual(resp.template_name, 'password_reset.html')
|
||||||
|
self.assertTrue(models.PasswordReset.objects.exists())
|
||||||
|
|
||||||
|
|
||||||
|
def test_register(self):
|
||||||
|
''' create a user '''
|
||||||
|
view = views.Register.as_view()
|
||||||
|
self.assertEqual(models.User.objects.count(), 1)
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nutria-user.user_nutria',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.cccc'
|
||||||
|
})
|
||||||
|
with patch('bookwyrm.views.authentication.login'):
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 2)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
nutria = models.User.objects.last()
|
||||||
|
self.assertEqual(nutria.username, 'nutria-user.user_nutria@%s' % DOMAIN)
|
||||||
|
self.assertEqual(nutria.localname, 'nutria-user.user_nutria')
|
||||||
|
self.assertEqual(nutria.local, True)
|
||||||
|
|
||||||
|
def test_register_trailing_space(self):
|
||||||
|
''' django handles this so weirdly '''
|
||||||
|
view = views.Register.as_view()
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nutria ',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.ccc'
|
||||||
|
})
|
||||||
|
with patch('bookwyrm.views.authentication.login'):
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 2)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
nutria = models.User.objects.last()
|
||||||
|
self.assertEqual(nutria.username, 'nutria@%s' % DOMAIN)
|
||||||
|
self.assertEqual(nutria.localname, 'nutria')
|
||||||
|
self.assertEqual(nutria.local, True)
|
||||||
|
|
||||||
|
def test_register_invalid_email(self):
|
||||||
|
''' gotta have an email '''
|
||||||
|
view = views.Register.as_view()
|
||||||
|
self.assertEqual(models.User.objects.count(), 1)
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nutria',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa'
|
||||||
|
})
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 1)
|
||||||
|
self.assertEqual(response.template_name, 'login.html')
|
||||||
|
|
||||||
|
def test_register_invalid_username(self):
|
||||||
|
''' gotta have an email '''
|
||||||
|
view = views.Register.as_view()
|
||||||
|
self.assertEqual(models.User.objects.count(), 1)
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nut@ria',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.ccc'
|
||||||
|
})
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 1)
|
||||||
|
self.assertEqual(response.template_name, 'login.html')
|
||||||
|
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nutr ia',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.ccc'
|
||||||
|
})
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 1)
|
||||||
|
self.assertEqual(response.template_name, 'login.html')
|
||||||
|
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nut@ria',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.ccc'
|
||||||
|
})
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 1)
|
||||||
|
self.assertEqual(response.template_name, 'login.html')
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_closed_instance(self):
|
||||||
|
''' you can't just register '''
|
||||||
|
view = views.Register.as_view()
|
||||||
|
self.settings.allow_registration = False
|
||||||
|
self.settings.save()
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nutria ',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.ccc'
|
||||||
|
})
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
def test_register_invite(self):
|
||||||
|
''' you can't just register '''
|
||||||
|
view = views.Register.as_view()
|
||||||
|
self.settings.allow_registration = False
|
||||||
|
self.settings.save()
|
||||||
|
models.SiteInvite.objects.create(
|
||||||
|
code='testcode', user=self.local_user, use_limit=1)
|
||||||
|
self.assertEqual(models.SiteInvite.objects.get().times_used, 0)
|
||||||
|
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nutria',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.ccc',
|
||||||
|
'invite_code': 'testcode'
|
||||||
|
})
|
||||||
|
with patch('bookwyrm.views.authentication.login'):
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 2)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(models.SiteInvite.objects.get().times_used, 1)
|
||||||
|
|
||||||
|
# invite already used to max capacity
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nutria2',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.ccc',
|
||||||
|
'invite_code': 'testcode'
|
||||||
|
})
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 2)
|
||||||
|
|
||||||
|
# bad invite code
|
||||||
|
request = self.factory.post(
|
||||||
|
'register/',
|
||||||
|
{
|
||||||
|
'localname': 'nutria3',
|
||||||
|
'password': 'mouseword',
|
||||||
|
'email': 'aa@bb.ccc',
|
||||||
|
'invite_code': 'dkfkdjgdfkjgkdfj'
|
||||||
|
})
|
||||||
|
with self.assertRaises(Http404):
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(models.User.objects.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_change(self):
|
||||||
|
''' change password '''
|
||||||
|
view = views.ChangePassword.as_view()
|
||||||
|
password_hash = self.local_user.password
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'password': 'hi',
|
||||||
|
'confirm-password': 'hi'
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.password.login'):
|
||||||
|
view(request)
|
||||||
|
self.assertNotEqual(self.local_user.password, password_hash)
|
||||||
|
|
||||||
|
def test_password_change_mismatch(self):
|
||||||
|
''' change password '''
|
||||||
|
view = views.ChangePassword.as_view()
|
||||||
|
password_hash = self.local_user.password
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'password': 'hi',
|
||||||
|
'confirm-password': 'hihi'
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
view(request)
|
||||||
|
self.assertEqual(self.local_user.password, password_hash)
|
119
bookwyrm/tests/views/test_author.py
Normal file
119
bookwyrm/tests/views/test_author.py
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import forms, models, views
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorViews(TestCase):
|
||||||
|
''' author views'''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.group = Group.objects.create(name='editor')
|
||||||
|
self.group.permissions.add(
|
||||||
|
Permission.objects.create(
|
||||||
|
name='edit_book',
|
||||||
|
codename='edit_book',
|
||||||
|
content_type=ContentType.objects.get_for_model(models.User)).id
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_author_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Author.as_view()
|
||||||
|
author = models.Author.objects.create(name='Jessica')
|
||||||
|
request = self.factory.get('')
|
||||||
|
with patch('bookwyrm.views.author.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, author.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'author.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get('')
|
||||||
|
with patch('bookwyrm.views.author.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, author.id)
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_author_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.EditAuthor.as_view()
|
||||||
|
author = models.Author.objects.create(name='Test Author')
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
|
||||||
|
result = view(request, author.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'edit_author.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_author(self):
|
||||||
|
''' edit an author '''
|
||||||
|
view = views.EditAuthor.as_view()
|
||||||
|
author = models.Author.objects.create(name='Test Author')
|
||||||
|
self.local_user.groups.add(self.group)
|
||||||
|
form = forms.AuthorForm(instance=author)
|
||||||
|
form.data['name'] = 'New Name'
|
||||||
|
form.data['last_edited_by'] = self.local_user.id
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, author.id)
|
||||||
|
author.refresh_from_db()
|
||||||
|
self.assertEqual(author.name, 'New Name')
|
||||||
|
self.assertEqual(author.last_edited_by, self.local_user)
|
||||||
|
|
||||||
|
def test_edit_author_non_editor(self):
|
||||||
|
''' edit an author with invalid post data'''
|
||||||
|
view = views.EditAuthor.as_view()
|
||||||
|
author = models.Author.objects.create(name='Test Author')
|
||||||
|
form = forms.AuthorForm(instance=author)
|
||||||
|
form.data['name'] = 'New Name'
|
||||||
|
form.data['last_edited_by'] = self.local_user.id
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
view(request, author.id)
|
||||||
|
author.refresh_from_db()
|
||||||
|
self.assertEqual(author.name, 'Test Author')
|
||||||
|
|
||||||
|
def test_edit_author_invalid_form(self):
|
||||||
|
''' edit an author with invalid post data'''
|
||||||
|
view = views.EditAuthor.as_view()
|
||||||
|
author = models.Author.objects.create(name='Test Author')
|
||||||
|
self.local_user.groups.add(self.group)
|
||||||
|
form = forms.AuthorForm(instance=author)
|
||||||
|
form.data['name'] = ''
|
||||||
|
form.data['last_edited_by'] = self.local_user.id
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
resp = view(request, author.id)
|
||||||
|
author.refresh_from_db()
|
||||||
|
self.assertEqual(author.name, 'Test Author')
|
||||||
|
self.assertEqual(resp.template_name, 'edit_author.html')
|
127
bookwyrm/tests/views/test_book.py
Normal file
127
bookwyrm/tests/views/test_book.py
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import forms, models, views
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
|
||||||
|
|
||||||
|
class BookViews(TestCase):
|
||||||
|
''' books books books '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.group = Group.objects.create(name='editor')
|
||||||
|
self.group.permissions.add(
|
||||||
|
Permission.objects.create(
|
||||||
|
name='edit_book',
|
||||||
|
codename='edit_book',
|
||||||
|
content_type=ContentType.objects.get_for_model(models.User)).id
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_book_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Book.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.books.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.book.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'book.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get('')
|
||||||
|
with patch('bookwyrm.views.books.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, self.book.id)
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_book_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.EditBook.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
result = view(request, self.book.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'edit_book.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_book(self):
|
||||||
|
''' lets a user edit a book '''
|
||||||
|
view = views.EditBook.as_view()
|
||||||
|
self.local_user.groups.add(self.group)
|
||||||
|
form = forms.EditionForm(instance=self.book)
|
||||||
|
form.data['title'] = 'New Title'
|
||||||
|
form.data['last_edited_by'] = self.local_user.id
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, self.book.id)
|
||||||
|
self.book.refresh_from_db()
|
||||||
|
self.assertEqual(self.book.title, 'New Title')
|
||||||
|
|
||||||
|
|
||||||
|
def test_switch_edition(self):
|
||||||
|
''' updates user's relationships to a book '''
|
||||||
|
work = models.Work.objects.create(title='test work')
|
||||||
|
edition1 = models.Edition.objects.create(
|
||||||
|
title='first ed', parent_work=work)
|
||||||
|
edition2 = models.Edition.objects.create(
|
||||||
|
title='second ed', parent_work=work)
|
||||||
|
shelf = models.Shelf.objects.create(
|
||||||
|
name='Test Shelf', user=self.local_user)
|
||||||
|
shelf.books.add(edition1)
|
||||||
|
models.ReadThrough.objects.create(
|
||||||
|
user=self.local_user, book=edition1)
|
||||||
|
|
||||||
|
self.assertEqual(models.ShelfBook.objects.get().book, edition1)
|
||||||
|
self.assertEqual(models.ReadThrough.objects.get().book, edition1)
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'edition': edition2.id
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.switch_edition(request)
|
||||||
|
|
||||||
|
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
|
||||||
|
self.assertEqual(models.ReadThrough.objects.get().book, edition2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_editions_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Editions.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
with patch('bookwyrm.views.books.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.work.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'editions.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get('')
|
||||||
|
with patch('bookwyrm.views.books.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, self.work.id)
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
28
bookwyrm/tests/views/test_direct_message.py
Normal file
28
bookwyrm/tests/views/test_direct_message.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm import views
|
||||||
|
|
||||||
|
|
||||||
|
class DirectMessageViews(TestCase):
|
||||||
|
''' dms '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.mouse', 'password',
|
||||||
|
local=True, localname='mouse')
|
||||||
|
|
||||||
|
|
||||||
|
def test_direct_messages_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.DirectMessage.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'direct_messages.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
108
bookwyrm/tests/views/test_follow.py
Normal file
108
bookwyrm/tests/views/test_follow.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
|
||||||
|
|
||||||
|
class BookViews(TestCase):
|
||||||
|
''' books books books '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
with patch('bookwyrm.models.user.set_remote_server'):
|
||||||
|
self.remote_user = models.User.objects.create_user(
|
||||||
|
'rat', 'rat@email.com', 'ratword',
|
||||||
|
local=False,
|
||||||
|
remote_id='https://example.com/users/rat',
|
||||||
|
inbox='https://example.com/users/rat/inbox',
|
||||||
|
outbox='https://example.com/users/rat/outbox',
|
||||||
|
)
|
||||||
|
self.group = Group.objects.create(name='editor')
|
||||||
|
self.group.permissions.add(
|
||||||
|
Permission.objects.create(
|
||||||
|
name='edit_book',
|
||||||
|
codename='edit_book',
|
||||||
|
content_type=ContentType.objects.get_for_model(models.User)).id
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_handle_follow(self):
|
||||||
|
''' send a follow request '''
|
||||||
|
request = self.factory.post('', {'user': self.remote_user.username})
|
||||||
|
request.user = self.local_user
|
||||||
|
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.follow(request)
|
||||||
|
|
||||||
|
rel = models.UserFollowRequest.objects.get()
|
||||||
|
|
||||||
|
self.assertEqual(rel.user_subject, self.local_user)
|
||||||
|
self.assertEqual(rel.user_object, self.remote_user)
|
||||||
|
self.assertEqual(rel.status, 'follow_request')
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_unfollow(self):
|
||||||
|
''' send an unfollow '''
|
||||||
|
request = self.factory.post('', {'user': self.remote_user.username})
|
||||||
|
request.user = self.local_user
|
||||||
|
self.remote_user.followers.add(self.local_user)
|
||||||
|
self.assertEqual(self.remote_user.followers.count(), 1)
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.unfollow(request)
|
||||||
|
|
||||||
|
self.assertEqual(self.remote_user.followers.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_accept(self):
|
||||||
|
''' accept a follow request '''
|
||||||
|
request = self.factory.post('', {'user': self.remote_user.username})
|
||||||
|
request.user = self.local_user
|
||||||
|
rel = models.UserFollowRequest.objects.create(
|
||||||
|
user_subject=self.remote_user,
|
||||||
|
user_object=self.local_user
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.accept_follow_request(request)
|
||||||
|
# request should be deleted
|
||||||
|
self.assertEqual(
|
||||||
|
models.UserFollowRequest.objects.filter(id=rel.id).count(), 0
|
||||||
|
)
|
||||||
|
# follow relationship should exist
|
||||||
|
self.assertEqual(self.local_user.followers.first(), self.remote_user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_reject(self):
|
||||||
|
''' reject a follow request '''
|
||||||
|
request = self.factory.post('', {'user': self.remote_user.username})
|
||||||
|
request.user = self.local_user
|
||||||
|
rel = models.UserFollowRequest.objects.create(
|
||||||
|
user_subject=self.remote_user,
|
||||||
|
user_object=self.local_user
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.delete_follow_request(request)
|
||||||
|
# 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
|
||||||
|
)
|
250
bookwyrm/tests/views/test_helpers.py
Normal file
250
bookwyrm/tests/views/test_helpers.py
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
import pathlib
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
import responses
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.settings import USER_AGENT
|
||||||
|
|
||||||
|
class ViewsHelpers(TestCase):
|
||||||
|
''' viewing and creating statuses '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Test Book',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath(
|
||||||
|
'../data/ap_user.json'
|
||||||
|
)
|
||||||
|
self.userdata = json.loads(datafile.read_bytes())
|
||||||
|
del self.userdata['icon']
|
||||||
|
self.shelf = models.Shelf.objects.create(
|
||||||
|
name='Test Shelf',
|
||||||
|
identifier='test-shelf',
|
||||||
|
user=self.local_user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_edition(self):
|
||||||
|
''' given an edition or a work, returns an edition '''
|
||||||
|
self.assertEqual(
|
||||||
|
views.helpers.get_edition(self.book.id), self.book)
|
||||||
|
self.assertEqual(
|
||||||
|
views.helpers.get_edition(self.work.id), self.book)
|
||||||
|
|
||||||
|
def test_get_user_from_username(self):
|
||||||
|
''' works for either localname or username '''
|
||||||
|
self.assertEqual(
|
||||||
|
views.helpers.get_user_from_username('mouse'), self.local_user)
|
||||||
|
self.assertEqual(
|
||||||
|
views.helpers.get_user_from_username(
|
||||||
|
'mouse@local.com'), self.local_user)
|
||||||
|
with self.assertRaises(models.User.DoesNotExist):
|
||||||
|
views.helpers.get_user_from_username('mojfse@example.com')
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_api_request(self):
|
||||||
|
''' should it return html or json '''
|
||||||
|
request = self.factory.get('/path')
|
||||||
|
request.headers = {'Accept': 'application/json'}
|
||||||
|
self.assertTrue(views.helpers.is_api_request(request))
|
||||||
|
|
||||||
|
request = self.factory.get('/path.json')
|
||||||
|
request.headers = {'Accept': 'Praise'}
|
||||||
|
self.assertTrue(views.helpers.is_api_request(request))
|
||||||
|
|
||||||
|
request = self.factory.get('/path')
|
||||||
|
request.headers = {'Accept': 'Praise'}
|
||||||
|
self.assertFalse(views.helpers.is_api_request(request))
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_activity_feed(self):
|
||||||
|
''' loads statuses '''
|
||||||
|
rat = models.User.objects.create_user(
|
||||||
|
'rat', 'rat@rat.rat', 'password', local=True)
|
||||||
|
|
||||||
|
public_status = models.Comment.objects.create(
|
||||||
|
content='public status', book=self.book, user=self.local_user)
|
||||||
|
direct_status = models.Status.objects.create(
|
||||||
|
content='direct', user=self.local_user, privacy='direct')
|
||||||
|
|
||||||
|
rat_public = models.Status.objects.create(
|
||||||
|
content='blah blah', user=rat)
|
||||||
|
rat_unlisted = models.Status.objects.create(
|
||||||
|
content='blah blah', user=rat, privacy='unlisted')
|
||||||
|
remote_status = models.Status.objects.create(
|
||||||
|
content='blah blah', user=self.remote_user)
|
||||||
|
followers_status = models.Status.objects.create(
|
||||||
|
content='blah', user=rat, privacy='followers')
|
||||||
|
rat_mention = models.Status.objects.create(
|
||||||
|
content='blah blah blah', user=rat, privacy='followers')
|
||||||
|
rat_mention.mention_users.set([self.local_user])
|
||||||
|
|
||||||
|
statuses = views.helpers.get_activity_feed(
|
||||||
|
self.local_user,
|
||||||
|
['public', 'unlisted', 'followers'],
|
||||||
|
following_only=True,
|
||||||
|
queryset=models.Comment.objects
|
||||||
|
)
|
||||||
|
self.assertEqual(len(statuses), 1)
|
||||||
|
self.assertEqual(statuses[0], public_status)
|
||||||
|
|
||||||
|
statuses = views.helpers.get_activity_feed(
|
||||||
|
self.local_user,
|
||||||
|
['public', 'followers'],
|
||||||
|
local_only=True
|
||||||
|
)
|
||||||
|
self.assertEqual(len(statuses), 2)
|
||||||
|
self.assertEqual(statuses[1], public_status)
|
||||||
|
self.assertEqual(statuses[0], rat_public)
|
||||||
|
|
||||||
|
statuses = views.helpers.get_activity_feed(self.local_user, 'direct')
|
||||||
|
self.assertEqual(len(statuses), 1)
|
||||||
|
self.assertEqual(statuses[0], direct_status)
|
||||||
|
|
||||||
|
statuses = views.helpers.get_activity_feed(
|
||||||
|
self.local_user,
|
||||||
|
['public', 'followers'],
|
||||||
|
)
|
||||||
|
self.assertEqual(len(statuses), 3)
|
||||||
|
self.assertEqual(statuses[2], public_status)
|
||||||
|
self.assertEqual(statuses[1], rat_public)
|
||||||
|
self.assertEqual(statuses[0], remote_status)
|
||||||
|
|
||||||
|
statuses = views.helpers.get_activity_feed(
|
||||||
|
self.local_user,
|
||||||
|
['public', 'unlisted', 'followers'],
|
||||||
|
following_only=True
|
||||||
|
)
|
||||||
|
self.assertEqual(len(statuses), 2)
|
||||||
|
self.assertEqual(statuses[1], public_status)
|
||||||
|
self.assertEqual(statuses[0], rat_mention)
|
||||||
|
|
||||||
|
rat.followers.add(self.local_user)
|
||||||
|
statuses = views.helpers.get_activity_feed(
|
||||||
|
self.local_user,
|
||||||
|
['public', 'unlisted', 'followers'],
|
||||||
|
following_only=True
|
||||||
|
)
|
||||||
|
self.assertEqual(len(statuses), 5)
|
||||||
|
self.assertEqual(statuses[4], public_status)
|
||||||
|
self.assertEqual(statuses[3], rat_public)
|
||||||
|
self.assertEqual(statuses[2], rat_unlisted)
|
||||||
|
self.assertEqual(statuses[1], followers_status)
|
||||||
|
self.assertEqual(statuses[0], rat_mention)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_bookwyrm_request(self):
|
||||||
|
''' checks if a request came from a bookwyrm instance '''
|
||||||
|
request = self.factory.get('', {'q': 'Test Book'})
|
||||||
|
self.assertFalse(views.helpers.is_bookworm_request(request))
|
||||||
|
|
||||||
|
request = self.factory.get(
|
||||||
|
'', {'q': 'Test Book'},
|
||||||
|
HTTP_USER_AGENT=\
|
||||||
|
"http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)"
|
||||||
|
)
|
||||||
|
self.assertFalse(views.helpers.is_bookworm_request(request))
|
||||||
|
|
||||||
|
request = self.factory.get(
|
||||||
|
'', {'q': 'Test Book'}, HTTP_USER_AGENT=USER_AGENT)
|
||||||
|
self.assertTrue(views.helpers.is_bookworm_request(request))
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_user(self):
|
||||||
|
''' simple database lookup by username '''
|
||||||
|
result = views.helpers.handle_remote_webfinger('@mouse@local.com')
|
||||||
|
self.assertEqual(result, self.local_user)
|
||||||
|
|
||||||
|
result = views.helpers.handle_remote_webfinger('mouse@local.com')
|
||||||
|
self.assertEqual(result, self.local_user)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_load_user(self):
|
||||||
|
''' find a remote user using webfinger '''
|
||||||
|
username = 'mouse@example.com'
|
||||||
|
wellknown = {
|
||||||
|
"subject": "acct:mouse@example.com",
|
||||||
|
"links": [{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": "https://example.com/user/mouse"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
'https://example.com/.well-known/webfinger?resource=acct:%s' \
|
||||||
|
% username,
|
||||||
|
json=wellknown,
|
||||||
|
status=200)
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
'https://example.com/user/mouse',
|
||||||
|
json=self.userdata,
|
||||||
|
status=200)
|
||||||
|
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||||
|
result = views.helpers.handle_remote_webfinger('@mouse@example.com')
|
||||||
|
self.assertIsInstance(result, models.User)
|
||||||
|
self.assertEqual(result.username, 'mouse@example.com')
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_reading_status_to_read(self):
|
||||||
|
''' posts shelve activities '''
|
||||||
|
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.helpers.handle_reading_status(
|
||||||
|
self.local_user, shelf, self.book, 'public')
|
||||||
|
status = models.GeneratedNote.objects.get()
|
||||||
|
self.assertEqual(status.user, self.local_user)
|
||||||
|
self.assertEqual(status.mention_books.first(), self.book)
|
||||||
|
self.assertEqual(status.content, 'wants to read')
|
||||||
|
|
||||||
|
def test_handle_reading_status_reading(self):
|
||||||
|
''' posts shelve activities '''
|
||||||
|
shelf = self.local_user.shelf_set.get(identifier='reading')
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.helpers.handle_reading_status(
|
||||||
|
self.local_user, shelf, self.book, 'public')
|
||||||
|
status = models.GeneratedNote.objects.get()
|
||||||
|
self.assertEqual(status.user, self.local_user)
|
||||||
|
self.assertEqual(status.mention_books.first(), self.book)
|
||||||
|
self.assertEqual(status.content, 'started reading')
|
||||||
|
|
||||||
|
def test_handle_reading_status_read(self):
|
||||||
|
''' posts shelve activities '''
|
||||||
|
shelf = self.local_user.shelf_set.get(identifier='read')
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.helpers.handle_reading_status(
|
||||||
|
self.local_user, shelf, self.book, 'public')
|
||||||
|
status = models.GeneratedNote.objects.get()
|
||||||
|
self.assertEqual(status.user, self.local_user)
|
||||||
|
self.assertEqual(status.mention_books.first(), self.book)
|
||||||
|
self.assertEqual(status.content, 'finished reading')
|
||||||
|
|
||||||
|
def test_handle_reading_status_other(self):
|
||||||
|
''' posts shelve activities '''
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.helpers.handle_reading_status(
|
||||||
|
self.local_user, self.shelf, self.book, 'public')
|
||||||
|
self.assertFalse(models.GeneratedNote.objects.exists())
|
43
bookwyrm/tests/views/test_import.py
Normal file
43
bookwyrm/tests/views/test_import.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm import views
|
||||||
|
|
||||||
|
|
||||||
|
class ImportViews(TestCase):
|
||||||
|
''' goodreads import views '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.mouse', 'password',
|
||||||
|
local=True, localname='mouse')
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Import.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'import.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_status(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.ImportStatus.as_view()
|
||||||
|
import_job = models.ImportJob.objects.create(user=self.local_user)
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.tasks.app.AsyncResult') as async_result:
|
||||||
|
async_result.return_value = []
|
||||||
|
result = view(request, import_job.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'import_status.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
152
bookwyrm/tests/views/test_interaction.py
Normal file
152
bookwyrm/tests/views/test_interaction.py
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
|
||||||
|
|
||||||
|
class InteractionViews(TestCase):
|
||||||
|
''' viewing and creating statuses '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
with patch('bookwyrm.models.user.set_remote_server'):
|
||||||
|
self.remote_user = models.User.objects.create_user(
|
||||||
|
'rat', 'rat@email.com', 'ratword',
|
||||||
|
local=False,
|
||||||
|
remote_id='https://example.com/users/rat',
|
||||||
|
inbox='https://example.com/users/rat/inbox',
|
||||||
|
outbox='https://example.com/users/rat/outbox',
|
||||||
|
)
|
||||||
|
|
||||||
|
work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=work
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_favorite(self):
|
||||||
|
''' create and broadcast faving a status '''
|
||||||
|
view = views.Favorite.as_view()
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.remote_user
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content='hi')
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, status.id)
|
||||||
|
fav = models.Favorite.objects.get()
|
||||||
|
self.assertEqual(fav.status, status)
|
||||||
|
self.assertEqual(fav.user, self.remote_user)
|
||||||
|
|
||||||
|
notification = models.Notification.objects.get()
|
||||||
|
self.assertEqual(notification.notification_type, 'FAVORITE')
|
||||||
|
self.assertEqual(notification.user, self.local_user)
|
||||||
|
self.assertEqual(notification.related_user, self.remote_user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_unfavorite(self):
|
||||||
|
''' unfav a status '''
|
||||||
|
view = views.Unfavorite.as_view()
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.remote_user
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content='hi')
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.Favorite.as_view()(request, status.id)
|
||||||
|
|
||||||
|
self.assertEqual(models.Favorite.objects.count(), 1)
|
||||||
|
self.assertEqual(models.Notification.objects.count(), 1)
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, status.id)
|
||||||
|
self.assertEqual(models.Favorite.objects.count(), 0)
|
||||||
|
self.assertEqual(models.Notification.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_boost(self):
|
||||||
|
''' boost a status '''
|
||||||
|
view = views.Boost.as_view()
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.remote_user
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content='hi')
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, status.id)
|
||||||
|
|
||||||
|
boost = models.Boost.objects.get()
|
||||||
|
self.assertEqual(boost.boosted_status, status)
|
||||||
|
self.assertEqual(boost.user, self.remote_user)
|
||||||
|
self.assertEqual(boost.privacy, 'public')
|
||||||
|
|
||||||
|
notification = models.Notification.objects.get()
|
||||||
|
self.assertEqual(notification.notification_type, 'BOOST')
|
||||||
|
self.assertEqual(notification.user, self.local_user)
|
||||||
|
self.assertEqual(notification.related_user, self.remote_user)
|
||||||
|
self.assertEqual(notification.related_status, status)
|
||||||
|
|
||||||
|
def test_handle_boost_unlisted(self):
|
||||||
|
''' boost a status '''
|
||||||
|
view = views.Boost.as_view()
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.local_user
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content='hi', privacy='unlisted')
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, status.id)
|
||||||
|
|
||||||
|
boost = models.Boost.objects.get()
|
||||||
|
self.assertEqual(boost.privacy, 'unlisted')
|
||||||
|
|
||||||
|
def test_handle_boost_private(self):
|
||||||
|
''' boost a status '''
|
||||||
|
view = views.Boost.as_view()
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.local_user
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content='hi', privacy='followers')
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, status.id)
|
||||||
|
self.assertFalse(models.Boost.objects.exists())
|
||||||
|
|
||||||
|
def test_handle_boost_twice(self):
|
||||||
|
''' boost a status '''
|
||||||
|
view = views.Boost.as_view()
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.local_user
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content='hi')
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, status.id)
|
||||||
|
view(request, status.id)
|
||||||
|
self.assertEqual(models.Boost.objects.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_unboost(self):
|
||||||
|
''' undo a boost '''
|
||||||
|
view = views.Unboost.as_view()
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.remote_user
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content='hi')
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.Boost.as_view()(request, status.id)
|
||||||
|
|
||||||
|
self.assertEqual(models.Boost.objects.count(), 1)
|
||||||
|
self.assertEqual(models.Notification.objects.count(), 1)
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, status.id)
|
||||||
|
self.assertEqual(models.Boost.objects.count(), 0)
|
||||||
|
self.assertEqual(models.Notification.objects.count(), 0)
|
48
bookwyrm/tests/views/test_invite.py
Normal file
48
bookwyrm/tests/views/test_invite.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm import views
|
||||||
|
|
||||||
|
|
||||||
|
class InviteViews(TestCase):
|
||||||
|
''' every response to a get request, html or json '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.mouse', 'password',
|
||||||
|
local=True, localname='mouse')
|
||||||
|
|
||||||
|
|
||||||
|
def test_invite_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Invite.as_view()
|
||||||
|
models.SiteInvite.objects.create(code='hi', user=self.local_user)
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = AnonymousUser
|
||||||
|
# why?? this is annoying.
|
||||||
|
request.user.is_authenticated = False
|
||||||
|
with patch('bookwyrm.models.site.SiteInvite.valid') as invite:
|
||||||
|
invite.return_value = True
|
||||||
|
result = view(request, 'hi')
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'invite.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_manage_invites(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.ManageInvites.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'manage_invites.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
84
bookwyrm/tests/views/test_landing.py
Normal file
84
bookwyrm/tests/views/test_landing.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm import views
|
||||||
|
|
||||||
|
|
||||||
|
class LandingViews(TestCase):
|
||||||
|
''' pages you land on without really trying '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.mouse', 'password',
|
||||||
|
local=True, localname='mouse')
|
||||||
|
self.anonymous_user = AnonymousUser
|
||||||
|
self.anonymous_user.is_authenticated = False
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Home.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assertEqual(result.template_name, 'feed.html')
|
||||||
|
|
||||||
|
request.user = self.anonymous_user
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assertEqual(result.template_name, 'discover.html')
|
||||||
|
|
||||||
|
|
||||||
|
def test_about_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.About.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'about.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_feed(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Feed.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request, 'local')
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'feed.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_discover(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Discover.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'discover.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_suggested_book(self):
|
||||||
|
''' gets books the ~*~ algorithm ~*~ thinks you want to post about '''
|
||||||
|
models.ShelfBook.objects.create(
|
||||||
|
book=self.book,
|
||||||
|
added_by=self.local_user,
|
||||||
|
shelf=self.local_user.shelf_set.get(identifier='reading')
|
||||||
|
)
|
||||||
|
suggestions = views.landing.get_suggested_books(self.local_user)
|
||||||
|
self.assertEqual(suggestions[0]['name'], 'Currently Reading')
|
||||||
|
self.assertEqual(suggestions[0]['books'][0], self.book)
|
41
bookwyrm/tests/views/test_notifications.py
Normal file
41
bookwyrm/tests/views/test_notifications.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm import views
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationViews(TestCase):
|
||||||
|
''' notifications '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.mouse', 'password',
|
||||||
|
local=True, localname='mouse')
|
||||||
|
|
||||||
|
def test_notifications_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Notifications.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'notifications.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_clear_notifications(self):
|
||||||
|
''' erase notifications '''
|
||||||
|
models.Notification.objects.create(
|
||||||
|
user=self.local_user, notification_type='MENTION')
|
||||||
|
models.Notification.objects.create(
|
||||||
|
user=self.local_user, notification_type='MENTION', read=True)
|
||||||
|
self.assertEqual(models.Notification.objects.count(), 2)
|
||||||
|
view = views.Notifications.as_view()
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request)
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
self.assertEqual(models.Notification.objects.count(), 1)
|
88
bookwyrm/tests/views/test_outbox.py
Normal file
88
bookwyrm/tests/views/test_outbox.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
''' sending out activities '''
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
|
class OutboxView(TestCase):
|
||||||
|
''' sends out activities '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we'll need some data '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=work
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_outbox(self):
|
||||||
|
''' returns user's statuses '''
|
||||||
|
request = self.factory.get('')
|
||||||
|
result = views.Outbox.as_view()(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, JsonResponse)
|
||||||
|
|
||||||
|
def test_outbox_bad_method(self):
|
||||||
|
''' can't POST to outbox '''
|
||||||
|
request = self.factory.post('')
|
||||||
|
result = views.Outbox.as_view()(request, 'mouse')
|
||||||
|
self.assertEqual(result.status_code, 405)
|
||||||
|
|
||||||
|
def test_outbox_unknown_user(self):
|
||||||
|
''' should 404 for unknown and remote users '''
|
||||||
|
request = self.factory.post('')
|
||||||
|
result = views.Outbox.as_view()(request, 'beepboop')
|
||||||
|
self.assertEqual(result.status_code, 405)
|
||||||
|
result = views.Outbox.as_view()(request, 'rat')
|
||||||
|
self.assertEqual(result.status_code, 405)
|
||||||
|
|
||||||
|
def test_outbox_privacy(self):
|
||||||
|
''' don't show dms et cetera in outbox '''
|
||||||
|
models.Status.objects.create(
|
||||||
|
content='PRIVATE!!', user=self.local_user, privacy='direct')
|
||||||
|
models.Status.objects.create(
|
||||||
|
content='bffs ONLY', user=self.local_user, privacy='followers')
|
||||||
|
models.Status.objects.create(
|
||||||
|
content='unlisted status', user=self.local_user, privacy='unlisted')
|
||||||
|
models.Status.objects.create(
|
||||||
|
content='look at this', user=self.local_user, privacy='public')
|
||||||
|
|
||||||
|
request = self.factory.get('')
|
||||||
|
result = views.Outbox.as_view()(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, JsonResponse)
|
||||||
|
data = json.loads(result.content)
|
||||||
|
self.assertEqual(data['type'], 'OrderedCollection')
|
||||||
|
self.assertEqual(data['totalItems'], 2)
|
||||||
|
|
||||||
|
def test_outbox_filter(self):
|
||||||
|
''' if we only care about reviews, only get reviews '''
|
||||||
|
models.Review.objects.create(
|
||||||
|
content='look at this', name='hi', rating=1,
|
||||||
|
book=self.book, user=self.local_user)
|
||||||
|
models.Status.objects.create(
|
||||||
|
content='look at this', user=self.local_user)
|
||||||
|
|
||||||
|
request = self.factory.get('', {'type': 'bleh'})
|
||||||
|
result = views.Outbox.as_view()(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, JsonResponse)
|
||||||
|
data = json.loads(result.content)
|
||||||
|
self.assertEqual(data['type'], 'OrderedCollection')
|
||||||
|
self.assertEqual(data['totalItems'], 2)
|
||||||
|
|
||||||
|
request = self.factory.get('', {'type': 'Review'})
|
||||||
|
result = views.Outbox.as_view()(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, JsonResponse)
|
||||||
|
data = json.loads(result.content)
|
||||||
|
self.assertEqual(data['type'], 'OrderedCollection')
|
||||||
|
self.assertEqual(data['totalItems'], 1)
|
176
bookwyrm/tests/views/test_reading.py
Normal file
176
bookwyrm/tests/views/test_reading.py
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
import dateutil
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
|
||||||
|
class ReadingViews(TestCase):
|
||||||
|
''' viewing and creating statuses '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Test Book',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_reading(self):
|
||||||
|
''' begin a book '''
|
||||||
|
shelf = self.local_user.shelf_set.get(identifier='reading')
|
||||||
|
self.assertFalse(shelf.books.exists())
|
||||||
|
self.assertFalse(models.Status.objects.exists())
|
||||||
|
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'post-status': True,
|
||||||
|
'privacy': 'followers',
|
||||||
|
'start_date': '2020-01-05',
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
views.start_reading(request, self.book.id)
|
||||||
|
|
||||||
|
self.assertEqual(shelf.books.get(), self.book)
|
||||||
|
|
||||||
|
status = models.GeneratedNote.objects.get()
|
||||||
|
self.assertEqual(status.user, self.local_user)
|
||||||
|
self.assertEqual(status.mention_books.get(), self.book)
|
||||||
|
self.assertEqual(status.privacy, 'followers')
|
||||||
|
|
||||||
|
readthrough = models.ReadThrough.objects.get()
|
||||||
|
self.assertIsNotNone(readthrough.start_date)
|
||||||
|
self.assertIsNone(readthrough.finish_date)
|
||||||
|
self.assertEqual(readthrough.user, self.local_user)
|
||||||
|
self.assertEqual(readthrough.book, self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_reading_reshelf(self):
|
||||||
|
''' begin a book '''
|
||||||
|
to_read_shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||||
|
models.ShelfBook.objects.create(
|
||||||
|
shelf=to_read_shelf, book=self.book, added_by=self.local_user)
|
||||||
|
shelf = self.local_user.shelf_set.get(identifier='reading')
|
||||||
|
self.assertEqual(to_read_shelf.books.get(), self.book)
|
||||||
|
self.assertFalse(shelf.books.exists())
|
||||||
|
self.assertFalse(models.Status.objects.exists())
|
||||||
|
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.local_user
|
||||||
|
views.start_reading(request, self.book.id)
|
||||||
|
|
||||||
|
self.assertFalse(to_read_shelf.books.exists())
|
||||||
|
self.assertEqual(shelf.books.get(), self.book)
|
||||||
|
|
||||||
|
def test_finish_reading(self):
|
||||||
|
''' begin a book '''
|
||||||
|
shelf = self.local_user.shelf_set.get(identifier='read')
|
||||||
|
self.assertFalse(shelf.books.exists())
|
||||||
|
self.assertFalse(models.Status.objects.exists())
|
||||||
|
readthrough = models.ReadThrough.objects.create(
|
||||||
|
user=self.local_user,
|
||||||
|
start_date=timezone.now(),
|
||||||
|
book=self.book)
|
||||||
|
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'post-status': True,
|
||||||
|
'privacy': 'followers',
|
||||||
|
'finish_date': '2020-01-07',
|
||||||
|
'id': readthrough.id,
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
views.finish_reading(request, self.book.id)
|
||||||
|
|
||||||
|
self.assertEqual(shelf.books.get(), self.book)
|
||||||
|
|
||||||
|
status = models.GeneratedNote.objects.get()
|
||||||
|
self.assertEqual(status.user, self.local_user)
|
||||||
|
self.assertEqual(status.mention_books.get(), self.book)
|
||||||
|
self.assertEqual(status.privacy, 'followers')
|
||||||
|
|
||||||
|
readthrough = models.ReadThrough.objects.get()
|
||||||
|
self.assertIsNotNone(readthrough.start_date)
|
||||||
|
self.assertIsNotNone(readthrough.finish_date)
|
||||||
|
self.assertEqual(readthrough.user, self.local_user)
|
||||||
|
self.assertEqual(readthrough.book, self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_readthrough(self):
|
||||||
|
''' adding dates to an ongoing readthrough '''
|
||||||
|
start = timezone.make_aware(dateutil.parser.parse('2021-01-03'))
|
||||||
|
readthrough = models.ReadThrough.objects.create(
|
||||||
|
book=self.book, user=self.local_user, start_date=start)
|
||||||
|
request = self.factory.post(
|
||||||
|
'', {
|
||||||
|
'start_date': '2017-01-01',
|
||||||
|
'finish_date': '2018-03-07',
|
||||||
|
'book': '',
|
||||||
|
'id': readthrough.id,
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
views.edit_readthrough(request)
|
||||||
|
readthrough.refresh_from_db()
|
||||||
|
self.assertEqual(readthrough.start_date.year, 2017)
|
||||||
|
self.assertEqual(readthrough.start_date.month, 1)
|
||||||
|
self.assertEqual(readthrough.start_date.day, 1)
|
||||||
|
self.assertEqual(readthrough.finish_date.year, 2018)
|
||||||
|
self.assertEqual(readthrough.finish_date.month, 3)
|
||||||
|
self.assertEqual(readthrough.finish_date.day, 7)
|
||||||
|
self.assertEqual(readthrough.book, self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_readthrough(self):
|
||||||
|
''' remove a readthrough '''
|
||||||
|
readthrough = models.ReadThrough.objects.create(
|
||||||
|
book=self.book, user=self.local_user)
|
||||||
|
models.ReadThrough.objects.create(
|
||||||
|
book=self.book, user=self.local_user)
|
||||||
|
request = self.factory.post(
|
||||||
|
'', {
|
||||||
|
'id': readthrough.id,
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
views.delete_readthrough(request)
|
||||||
|
self.assertFalse(
|
||||||
|
models.ReadThrough.objects.filter(id=readthrough.id).exists())
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_readthrough(self):
|
||||||
|
''' adding new read dates '''
|
||||||
|
request = self.factory.post(
|
||||||
|
'', {
|
||||||
|
'start_date': '2017-01-01',
|
||||||
|
'finish_date': '2018-03-07',
|
||||||
|
'book': self.book.id,
|
||||||
|
'id': '',
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
views.create_readthrough(request)
|
||||||
|
readthrough = models.ReadThrough.objects.get()
|
||||||
|
self.assertEqual(readthrough.start_date.year, 2017)
|
||||||
|
self.assertEqual(readthrough.start_date.month, 1)
|
||||||
|
self.assertEqual(readthrough.start_date.day, 1)
|
||||||
|
self.assertEqual(readthrough.finish_date.year, 2018)
|
||||||
|
self.assertEqual(readthrough.finish_date.month, 3)
|
||||||
|
self.assertEqual(readthrough.finish_date.day, 7)
|
||||||
|
self.assertEqual(readthrough.book, self.book)
|
||||||
|
self.assertEqual(readthrough.user, self.local_user)
|
108
bookwyrm/tests/views/test_search.py
Normal file
108
bookwyrm/tests/views/test_search.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.connectors import abstract_connector
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class ShelfViews(TestCase):
|
||||||
|
''' tag views'''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Test Book',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
models.Connector.objects.create(
|
||||||
|
identifier='self',
|
||||||
|
connector_file='self_connector',
|
||||||
|
local=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_json_response(self):
|
||||||
|
''' searches local data only and returns book data in json format '''
|
||||||
|
view = views.Search.as_view()
|
||||||
|
# we need a connector for this, sorry
|
||||||
|
request = self.factory.get('', {'q': 'Test Book'})
|
||||||
|
with patch('bookwyrm.views.search.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
response = view(request)
|
||||||
|
self.assertIsInstance(response, JsonResponse)
|
||||||
|
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
self.assertEqual(data[0]['title'], 'Test Book')
|
||||||
|
self.assertEqual(
|
||||||
|
data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id))
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_html_response(self):
|
||||||
|
''' searches remote connectors '''
|
||||||
|
view = views.Search.as_view()
|
||||||
|
class TestConnector(abstract_connector.AbstractMinimalConnector):
|
||||||
|
''' nothing added here '''
|
||||||
|
def format_search_result(self, search_result):
|
||||||
|
pass
|
||||||
|
def get_or_create_book(self, remote_id):
|
||||||
|
pass
|
||||||
|
def parse_search_data(self, data):
|
||||||
|
pass
|
||||||
|
models.Connector.objects.create(
|
||||||
|
identifier='example.com',
|
||||||
|
connector_file='openlibrary',
|
||||||
|
base_url='https://example.com',
|
||||||
|
books_url='https://example.com/books',
|
||||||
|
covers_url='https://example.com/covers',
|
||||||
|
search_url='https://example.com/search?q=',
|
||||||
|
)
|
||||||
|
connector = TestConnector('example.com')
|
||||||
|
|
||||||
|
search_result = abstract_connector.SearchResult(
|
||||||
|
key='http://www.example.com/book/1',
|
||||||
|
title='Gideon the Ninth',
|
||||||
|
author='Tamsyn Muir',
|
||||||
|
year='2019',
|
||||||
|
connector=connector
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.factory.get('', {'q': 'Test Book'})
|
||||||
|
with patch('bookwyrm.views.search.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
with patch(
|
||||||
|
'bookwyrm.connectors.connector_manager.search') as manager:
|
||||||
|
manager.return_value = [search_result]
|
||||||
|
response = view(request)
|
||||||
|
self.assertIsInstance(response, TemplateResponse)
|
||||||
|
self.assertEqual(response.template_name, 'search_results.html')
|
||||||
|
self.assertEqual(
|
||||||
|
response.context_data['book_results'][0].title, 'Gideon the Ninth')
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_html_response_users(self):
|
||||||
|
''' searches remote connectors '''
|
||||||
|
view = views.Search.as_view()
|
||||||
|
request = self.factory.get('', {'q': 'mouse'})
|
||||||
|
with patch('bookwyrm.views.search.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
with patch('bookwyrm.connectors.connector_manager.search'):
|
||||||
|
response = view(request)
|
||||||
|
self.assertIsInstance(response, TemplateResponse)
|
||||||
|
self.assertEqual(response.template_name, 'search_results.html')
|
||||||
|
self.assertEqual(
|
||||||
|
response.context_data['user_results'][0], self.local_user)
|
192
bookwyrm/tests/views/test_shelf.py
Normal file
192
bookwyrm/tests/views/test_shelf.py
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
|
||||||
|
|
||||||
|
class ShelfViews(TestCase):
|
||||||
|
''' tag views'''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
self.shelf = models.Shelf.objects.create(
|
||||||
|
name='Test Shelf',
|
||||||
|
identifier='test-shelf',
|
||||||
|
user=self.local_user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shelf_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Shelf.as_view()
|
||||||
|
shelf = self.local_user.shelf_set.first()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.shelf.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.local_user.username, shelf.identifier)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'shelf.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
with patch('bookwyrm.views.shelf.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(
|
||||||
|
request, self.local_user.username, shelf.identifier)
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get('/?page=1')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.shelf.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(
|
||||||
|
request, self.local_user.username, shelf.identifier)
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_shelf_privacy(self):
|
||||||
|
''' set name or privacy on shelf '''
|
||||||
|
view = views.Shelf.as_view()
|
||||||
|
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||||
|
self.assertEqual(shelf.privacy, 'public')
|
||||||
|
|
||||||
|
request = self.factory.post(
|
||||||
|
'', {
|
||||||
|
'privacy': 'unlisted',
|
||||||
|
'user': self.local_user.id,
|
||||||
|
'name': 'To Read',
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
view(request, self.local_user.username, shelf.id)
|
||||||
|
shelf.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(shelf.privacy, 'unlisted')
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_shelf_name(self):
|
||||||
|
''' change the name of an editable shelf '''
|
||||||
|
view = views.Shelf.as_view()
|
||||||
|
shelf = models.Shelf.objects.create(
|
||||||
|
name='Test Shelf', user=self.local_user)
|
||||||
|
self.assertEqual(shelf.privacy, 'public')
|
||||||
|
|
||||||
|
request = self.factory.post(
|
||||||
|
'', {
|
||||||
|
'privacy': 'public',
|
||||||
|
'user': self.local_user.id,
|
||||||
|
'name': 'cool name'
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
view(request, request.user.username, shelf.id)
|
||||||
|
shelf.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(shelf.name, 'cool name')
|
||||||
|
self.assertEqual(shelf.identifier, 'testshelf-%d' % shelf.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_shelf_name_not_editable(self):
|
||||||
|
''' can't change the name of an non-editable shelf '''
|
||||||
|
view = views.Shelf.as_view()
|
||||||
|
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||||
|
self.assertEqual(shelf.privacy, 'public')
|
||||||
|
|
||||||
|
request = self.factory.post(
|
||||||
|
'', {
|
||||||
|
'privacy': 'public',
|
||||||
|
'user': self.local_user.id,
|
||||||
|
'name': 'cool name'
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
view(request, request.user.username, shelf.id)
|
||||||
|
|
||||||
|
self.assertEqual(shelf.name, 'To Read')
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_shelve(self):
|
||||||
|
''' shelve a book '''
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'book': self.book.id,
|
||||||
|
'shelf': self.shelf.identifier
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.shelve(request)
|
||||||
|
# make sure the book is on the shelf
|
||||||
|
self.assertEqual(self.shelf.books.get(), self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_shelve_to_read(self):
|
||||||
|
''' special behavior for the to-read shelf '''
|
||||||
|
shelf = models.Shelf.objects.get(identifier='to-read')
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'book': self.book.id,
|
||||||
|
'shelf': shelf.identifier
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.shelve(request)
|
||||||
|
# make sure the book is on the shelf
|
||||||
|
self.assertEqual(shelf.books.get(), self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_shelve_reading(self):
|
||||||
|
''' special behavior for the reading shelf '''
|
||||||
|
shelf = models.Shelf.objects.get(identifier='reading')
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'book': self.book.id,
|
||||||
|
'shelf': shelf.identifier
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.shelve(request)
|
||||||
|
# make sure the book is on the shelf
|
||||||
|
self.assertEqual(shelf.books.get(), self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_shelve_read(self):
|
||||||
|
''' special behavior for the read shelf '''
|
||||||
|
shelf = models.Shelf.objects.get(identifier='read')
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'book': self.book.id,
|
||||||
|
'shelf': shelf.identifier
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.shelve(request)
|
||||||
|
# make sure the book is on the shelf
|
||||||
|
self.assertEqual(shelf.books.get(), self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_unshelve(self):
|
||||||
|
''' remove a book from a shelf '''
|
||||||
|
self.shelf.books.add(self.book)
|
||||||
|
self.shelf.save()
|
||||||
|
self.assertEqual(self.shelf.books.count(), 1)
|
||||||
|
request = self.factory.post('', {
|
||||||
|
'book': self.book.id,
|
||||||
|
'shelf': self.shelf.id
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
views.unshelve(request)
|
||||||
|
self.assertEqual(self.shelf.books.count(), 0)
|
273
bookwyrm/tests/views/test_status.py
Normal file
273
bookwyrm/tests/views/test_status.py
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import forms, models, views
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class StatusViews(TestCase):
|
||||||
|
''' viewing and creating statuses '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
with patch('bookwyrm.models.user.set_remote_server'):
|
||||||
|
self.remote_user = models.User.objects.create_user(
|
||||||
|
'rat', 'rat@email.com', 'ratword',
|
||||||
|
local=False,
|
||||||
|
remote_id='https://example.com/users/rat',
|
||||||
|
inbox='https://example.com/users/rat/inbox',
|
||||||
|
outbox='https://example.com/users/rat/outbox',
|
||||||
|
)
|
||||||
|
|
||||||
|
work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=work
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Status.as_view()
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
content='hi', user=self.local_user)
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.status.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, 'mouse', status.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'status.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
with patch('bookwyrm.views.status.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, 'mouse', status.id)
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_replies_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Replies.as_view()
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
content='hi', user=self.local_user)
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.status.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, 'mouse', status.id)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'status.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
with patch('bookwyrm.views.status.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, 'mouse', status.id)
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_status(self):
|
||||||
|
''' create a status '''
|
||||||
|
view = views.CreateStatus.as_view()
|
||||||
|
form = forms.CommentForm({
|
||||||
|
'content': 'hi',
|
||||||
|
'user': self.local_user.id,
|
||||||
|
'book': self.book.id,
|
||||||
|
'privacy': 'public',
|
||||||
|
})
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, 'comment')
|
||||||
|
status = models.Comment.objects.get()
|
||||||
|
self.assertEqual(status.content, '<p>hi</p>')
|
||||||
|
self.assertEqual(status.user, self.local_user)
|
||||||
|
self.assertEqual(status.book, self.book)
|
||||||
|
|
||||||
|
def test_handle_status_reply(self):
|
||||||
|
''' create a status in reply to an existing status '''
|
||||||
|
view = views.CreateStatus.as_view()
|
||||||
|
user = models.User.objects.create_user(
|
||||||
|
'rat', 'rat@rat.com', 'password', local=True)
|
||||||
|
parent = models.Status.objects.create(
|
||||||
|
content='parent status', user=self.local_user)
|
||||||
|
form = forms.ReplyForm({
|
||||||
|
'content': 'hi',
|
||||||
|
'user': user.id,
|
||||||
|
'reply_parent': parent.id,
|
||||||
|
'privacy': 'public',
|
||||||
|
})
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = user
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, 'reply')
|
||||||
|
status = models.Status.objects.get(user=user)
|
||||||
|
self.assertEqual(status.content, '<p>hi</p>')
|
||||||
|
self.assertEqual(status.user, user)
|
||||||
|
self.assertEqual(
|
||||||
|
models.Notification.objects.get().user, self.local_user)
|
||||||
|
|
||||||
|
def test_handle_status_mentions(self):
|
||||||
|
''' @mention a user in a post '''
|
||||||
|
view = views.CreateStatus.as_view()
|
||||||
|
user = models.User.objects.create_user(
|
||||||
|
'rat@%s' % DOMAIN, 'rat@rat.com', 'password',
|
||||||
|
local=True, localname='rat')
|
||||||
|
form = forms.CommentForm({
|
||||||
|
'content': 'hi @rat',
|
||||||
|
'user': self.local_user.id,
|
||||||
|
'book': self.book.id,
|
||||||
|
'privacy': 'public',
|
||||||
|
})
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, 'comment')
|
||||||
|
status = models.Status.objects.get()
|
||||||
|
self.assertEqual(list(status.mention_users.all()), [user])
|
||||||
|
self.assertEqual(models.Notification.objects.get().user, user)
|
||||||
|
self.assertEqual(
|
||||||
|
status.content,
|
||||||
|
'<p>hi <a href="%s">@rat</a></p>' % user.remote_id)
|
||||||
|
|
||||||
|
def test_handle_status_reply_with_mentions(self):
|
||||||
|
''' reply to a post with an @mention'ed user '''
|
||||||
|
view = views.CreateStatus.as_view()
|
||||||
|
user = models.User.objects.create_user(
|
||||||
|
'rat', 'rat@rat.com', 'password',
|
||||||
|
local=True, localname='rat')
|
||||||
|
form = forms.CommentForm({
|
||||||
|
'content': 'hi @rat@example.com',
|
||||||
|
'user': self.local_user.id,
|
||||||
|
'book': self.book.id,
|
||||||
|
'privacy': 'public',
|
||||||
|
})
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, 'comment')
|
||||||
|
status = models.Status.objects.get()
|
||||||
|
|
||||||
|
form = forms.ReplyForm({
|
||||||
|
'content': 'right',
|
||||||
|
'user': user.id,
|
||||||
|
'privacy': 'public',
|
||||||
|
'reply_parent': status.id
|
||||||
|
})
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = user
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, 'reply')
|
||||||
|
|
||||||
|
reply = models.Status.replies(status).first()
|
||||||
|
self.assertEqual(reply.content, '<p>right</p>')
|
||||||
|
self.assertEqual(reply.user, user)
|
||||||
|
self.assertTrue(self.remote_user in reply.mention_users.all())
|
||||||
|
self.assertTrue(self.local_user in reply.mention_users.all())
|
||||||
|
|
||||||
|
def test_find_mentions(self):
|
||||||
|
''' detect and look up @ mentions of users '''
|
||||||
|
user = models.User.objects.create_user(
|
||||||
|
'nutria@%s' % DOMAIN, 'nutria@nutria.com', 'password',
|
||||||
|
local=True, localname='nutria')
|
||||||
|
self.assertEqual(user.username, 'nutria@%s' % DOMAIN)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
list(views.status.find_mentions('@nutria'))[0],
|
||||||
|
('@nutria', user)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(views.status.find_mentions('leading text @nutria'))[0],
|
||||||
|
('@nutria', user)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(views.status.find_mentions(
|
||||||
|
'leading @nutria trailing text'))[0],
|
||||||
|
('@nutria', user)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(views.status.find_mentions(
|
||||||
|
'@rat@example.com'))[0],
|
||||||
|
('@rat@example.com', self.remote_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
multiple = list(views.status.find_mentions(
|
||||||
|
'@nutria and @rat@example.com'))
|
||||||
|
self.assertEqual(multiple[0], ('@nutria', user))
|
||||||
|
self.assertEqual(multiple[1], ('@rat@example.com', self.remote_user))
|
||||||
|
|
||||||
|
with patch('bookwyrm.views.status.handle_remote_webfinger') as rw:
|
||||||
|
rw.return_value = self.local_user
|
||||||
|
self.assertEqual(
|
||||||
|
list(views.status.find_mentions('@beep@beep.com'))[0],
|
||||||
|
('@beep@beep.com', self.local_user)
|
||||||
|
)
|
||||||
|
with patch('bookwyrm.views.status.handle_remote_webfinger') as rw:
|
||||||
|
rw.return_value = None
|
||||||
|
self.assertEqual(list(views.status.find_mentions(
|
||||||
|
'@beep@beep.com')), [])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
list(views.status.find_mentions('@nutria@%s' % DOMAIN))[0],
|
||||||
|
('@nutria@%s' % DOMAIN, user)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_format_links(self):
|
||||||
|
''' find and format urls into a tags '''
|
||||||
|
url = 'http://www.fish.com/'
|
||||||
|
self.assertEqual(
|
||||||
|
views.status.format_links(url),
|
||||||
|
'<a href="%s">www.fish.com/</a>' % url)
|
||||||
|
self.assertEqual(
|
||||||
|
views.status.format_links('(%s)' % url),
|
||||||
|
'(<a href="%s">www.fish.com/</a>)' % url)
|
||||||
|
url = 'https://archive.org/details/dli.granth.72113/page/n25/mode/2up'
|
||||||
|
self.assertEqual(
|
||||||
|
views.status.format_links(url),
|
||||||
|
'<a href="%s">' \
|
||||||
|
'archive.org/details/dli.granth.72113/page/n25/mode/2up</a>' \
|
||||||
|
% url)
|
||||||
|
url = 'https://openlibrary.org/search' \
|
||||||
|
'?q=arkady+strugatsky&mode=everything'
|
||||||
|
self.assertEqual(
|
||||||
|
views.status.format_links(url),
|
||||||
|
'<a href="%s">openlibrary.org/search' \
|
||||||
|
'?q=arkady+strugatsky&mode=everything</a>' % url)
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_markdown(self):
|
||||||
|
''' this is mostly handled in other places, but nonetheless '''
|
||||||
|
text = '_hi_ and http://fish.com is <marquee>rad</marquee>'
|
||||||
|
result = views.status.to_markdown(text)
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
'<p><em>hi</em> and <a href="http://fish.com">fish.com</a> ' \
|
||||||
|
'is rad</p>')
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_delete_status(self):
|
||||||
|
''' marks a status as deleted '''
|
||||||
|
view = views.DeleteStatus.as_view()
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content='hi')
|
||||||
|
self.assertFalse(status.deleted)
|
||||||
|
request = self.factory.post('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request, status.id)
|
||||||
|
status.refresh_from_db()
|
||||||
|
self.assertTrue(status.deleted)
|
99
bookwyrm/tests/views/test_tag.py
Normal file
99
bookwyrm/tests/views/test_tag.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
|
||||||
|
|
||||||
|
class TagViews(TestCase):
|
||||||
|
''' tag views'''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.group = Group.objects.create(name='editor')
|
||||||
|
self.group.permissions.add(
|
||||||
|
Permission.objects.create(
|
||||||
|
name='edit_book',
|
||||||
|
codename='edit_book',
|
||||||
|
content_type=ContentType.objects.get_for_model(models.User)).id
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tag_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Tag.as_view()
|
||||||
|
tag = models.Tag.objects.create(name='hi there')
|
||||||
|
models.UserTag.objects.create(
|
||||||
|
tag=tag, user=self.local_user, book=self.book)
|
||||||
|
request = self.factory.get('')
|
||||||
|
with patch('bookwyrm.views.tag.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, tag.identifier)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'tag.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get('')
|
||||||
|
with patch('bookwyrm.views.tag.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, tag.identifier)
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tag(self):
|
||||||
|
''' add a tag to a book '''
|
||||||
|
view = views.AddTag.as_view()
|
||||||
|
request = self.factory.post(
|
||||||
|
'', {
|
||||||
|
'name': 'A Tag!?',
|
||||||
|
'book': self.book.id,
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
tag = models.Tag.objects.get()
|
||||||
|
user_tag = models.UserTag.objects.get()
|
||||||
|
self.assertEqual(tag.name, 'A Tag!?')
|
||||||
|
self.assertEqual(tag.identifier, 'A+Tag%21%3F')
|
||||||
|
self.assertEqual(user_tag.user, self.local_user)
|
||||||
|
self.assertEqual(user_tag.book, self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_untag(self):
|
||||||
|
''' remove a tag from a book '''
|
||||||
|
view = views.RemoveTag.as_view()
|
||||||
|
tag = models.Tag.objects.create(name='A Tag!?')
|
||||||
|
models.UserTag.objects.create(
|
||||||
|
user=self.local_user, book=self.book, tag=tag)
|
||||||
|
request = self.factory.post(
|
||||||
|
'', {
|
||||||
|
'user': self.local_user.id,
|
||||||
|
'book': self.book.id,
|
||||||
|
'name': tag.name,
|
||||||
|
})
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request)
|
||||||
|
|
||||||
|
self.assertTrue(models.Tag.objects.filter(name='A Tag!?').exists())
|
||||||
|
self.assertFalse(models.UserTag.objects.exists())
|
99
bookwyrm/tests/views/test_user.py
Normal file
99
bookwyrm/tests/views/test_user.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import forms, models, views
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
|
||||||
|
|
||||||
|
class UserViews(TestCase):
|
||||||
|
''' view user and edit profile '''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.mouse', 'password',
|
||||||
|
local=True, localname='mouse')
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.User.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.user.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'user.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
with patch('bookwyrm.views.user.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_followers_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Followers.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.user.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'followers.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
with patch('bookwyrm.views.user.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_following_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.Following.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch('bookwyrm.views.user.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'following.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
with patch('bookwyrm.views.user.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
result = view(request, 'mouse')
|
||||||
|
self.assertIsInstance(result, ActivitypubResponse)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_profile_page(self):
|
||||||
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
|
view = views.EditUser.as_view()
|
||||||
|
request = self.factory.get('')
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
self.assertEqual(result.template_name, 'edit_user.html')
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_user(self):
|
||||||
|
''' use a form to update a user '''
|
||||||
|
view = views.EditUser.as_view()
|
||||||
|
form = forms.EditUserForm(instance=self.local_user)
|
||||||
|
form.data['name'] = 'New Name'
|
||||||
|
request = self.factory.post('', form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||||
|
view(request)
|
||||||
|
self.assertEqual(self.local_user.name, 'New Name')
|
175
bookwyrm/urls.py
175
bookwyrm/urls.py
|
@ -3,8 +3,7 @@ from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
|
|
||||||
from bookwyrm import incoming, outgoing, views, settings, wellknown
|
from bookwyrm import incoming, settings, views, wellknown
|
||||||
from bookwyrm import view_actions as actions
|
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
|
|
||||||
user_path = r'^user/(?P<username>%s)' % regex.username
|
user_path = r'^user/(?P<username>%s)' % regex.username
|
||||||
|
@ -31,7 +30,7 @@ urlpatterns = [
|
||||||
# federation endpoints
|
# federation endpoints
|
||||||
re_path(r'^inbox/?$', incoming.shared_inbox),
|
re_path(r'^inbox/?$', incoming.shared_inbox),
|
||||||
re_path(r'%s/inbox/?$' % local_user_path, incoming.inbox),
|
re_path(r'%s/inbox/?$' % local_user_path, incoming.inbox),
|
||||||
re_path(r'%s/outbox/?$' % local_user_path, outgoing.outbox),
|
re_path(r'%s/outbox/?$' % local_user_path, views.Outbox.as_view()),
|
||||||
|
|
||||||
# .well-known endpoints
|
# .well-known endpoints
|
||||||
re_path(r'^.well-known/webfinger/?$', wellknown.webfinger),
|
re_path(r'^.well-known/webfinger/?$', wellknown.webfinger),
|
||||||
|
@ -39,109 +38,97 @@ urlpatterns = [
|
||||||
re_path(r'^nodeinfo/2\.0/?$', wellknown.nodeinfo),
|
re_path(r'^nodeinfo/2\.0/?$', wellknown.nodeinfo),
|
||||||
re_path(r'^api/v1/instance/?$', wellknown.instance_info),
|
re_path(r'^api/v1/instance/?$', wellknown.instance_info),
|
||||||
re_path(r'^api/v1/instance/peers/?$', wellknown.peers),
|
re_path(r'^api/v1/instance/peers/?$', wellknown.peers),
|
||||||
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),
|
|
||||||
# TODO: robots.txt
|
|
||||||
|
|
||||||
# ui views
|
# authentication
|
||||||
re_path(r'^login/?$', views.login_page),
|
re_path(r'^login/?$', views.Login.as_view()),
|
||||||
re_path(r'^about/?$', views.about_page),
|
re_path(r'^register/?$', views.Register.as_view()),
|
||||||
re_path(r'^password-reset/?$', views.password_reset_request),
|
re_path(r'^logout/?$', views.Logout.as_view()),
|
||||||
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$', views.password_reset),
|
re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()),
|
||||||
re_path(r'^invite/?$', views.manage_invites),
|
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$',
|
||||||
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.invite_page),
|
views.PasswordReset.as_view()),
|
||||||
|
re_path(r'^change-password/?$', views.ChangePassword),
|
||||||
|
|
||||||
path('', views.home),
|
# invites
|
||||||
re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab),
|
re_path(r'^invite/?$', views.ManageInvites.as_view()),
|
||||||
re_path(r'^discover/?$', views.discover_page),
|
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.Invite.as_view()),
|
||||||
re_path(r'^notifications/?$', views.notifications_page),
|
|
||||||
re_path(r'^direct-messages/?$', views.direct_messages_page),
|
# landing pages
|
||||||
re_path(r'^import/?$', views.import_page),
|
re_path(r'^about/?$', views.About.as_view()),
|
||||||
re_path(r'^import-status/(\d+)/?$', views.import_status),
|
path('', views.Home.as_view()),
|
||||||
re_path(r'^user-edit/?$', views.edit_profile_page),
|
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
|
||||||
|
re_path(r'^discover/?$', views.Discover.as_view()),
|
||||||
|
re_path(r'^notifications/?$', views.Notifications.as_view()),
|
||||||
|
re_path(r'^direct-messages/?$', views.DirectMessage.as_view()),
|
||||||
|
|
||||||
|
# search
|
||||||
|
re_path(r'^search/?$', views.Search.as_view()),
|
||||||
|
|
||||||
|
# imports
|
||||||
|
re_path(r'^import/?$', views.Import.as_view()),
|
||||||
|
re_path(r'^import/(\d+)/?$', views.ImportStatus.as_view()),
|
||||||
|
|
||||||
# should return a ui view or activitypub json blob as requested
|
|
||||||
# users
|
# users
|
||||||
re_path(r'%s/?$' % user_path, views.user_page),
|
re_path(r'%s/?$' % user_path, views.User.as_view()),
|
||||||
re_path(r'%s\.json$' % local_user_path, views.user_page),
|
re_path(r'%s\.json$' % user_path, views.User.as_view()),
|
||||||
re_path(r'%s/?$' % local_user_path, views.user_page),
|
re_path(r'%s/shelves/?$' % user_path, views.user_shelves_page),
|
||||||
re_path(r'%s/shelves/?$' % local_user_path, views.user_shelves_page),
|
re_path(r'%s/followers(.json)?/?$' % user_path, views.Followers.as_view()),
|
||||||
re_path(r'%s/followers(.json)?/?$' % local_user_path, views.followers_page),
|
re_path(r'%s/following(.json)?/?$' % user_path, views.Following.as_view()),
|
||||||
re_path(r'%s/following(.json)?/?$' % local_user_path, views.following_page),
|
re_path(r'^edit-profile/?$', views.EditUser.as_view()),
|
||||||
|
|
||||||
# statuses
|
# statuses
|
||||||
re_path(r'%s(.json)?/?$' % status_path, views.status_page),
|
re_path(r'%s(.json)?/?$' % status_path, views.Status.as_view()),
|
||||||
re_path(r'%s/activity/?$' % status_path, views.status_page),
|
re_path(r'%s/activity/?$' % status_path, views.Status.as_view()),
|
||||||
re_path(r'%s/replies(.json)?/?$' % status_path, views.replies_page),
|
re_path(r'%s/replies(.json)?/?$' % status_path, views.Replies.as_view()),
|
||||||
|
re_path(r'^post/(?P<status_type>\w+)/?$', views.CreateStatus.as_view()),
|
||||||
|
re_path(r'^delete-status/(?P<status_id>\d+)/?$',
|
||||||
|
views.DeleteStatus.as_view()),
|
||||||
|
|
||||||
|
# interact
|
||||||
|
re_path(r'^favorite/(?P<status_id>\d+)/?$', views.Favorite.as_view()),
|
||||||
|
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', views.Unfavorite.as_view()),
|
||||||
|
re_path(r'^boost/(?P<status_id>\d+)/?$', views.Boost.as_view()),
|
||||||
|
re_path(r'^unboost/(?P<status_id>\d+)/?$', views.Unboost.as_view()),
|
||||||
|
|
||||||
# books
|
# books
|
||||||
re_path(r'%s(.json)?/?$' % book_path, views.book_page),
|
re_path(r'%s(.json)?/?$' % book_path, views.Book.as_view()),
|
||||||
re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
|
re_path(r'%s/edit/?$' % book_path, views.EditBook.as_view()),
|
||||||
re_path(r'^author/(?P<author_id>[\w\-]+)/edit/?$', views.edit_author_page),
|
re_path(r'%s/editions(.json)?/?$' % book_path, views.Editions.as_view()),
|
||||||
re_path(r'%s/editions(.json)?/?$' % book_path, views.editions_page),
|
re_path(r'^upload-cover/(?P<book_id>\d+)/?$', views.upload_cover),
|
||||||
|
re_path(r'^add-description/(?P<book_id>\d+)/?$', views.add_description),
|
||||||
|
re_path(r'^resolve-book/?$', views.resolve_book),
|
||||||
|
re_path(r'^switch-edition/?$', views.switch_edition),
|
||||||
|
|
||||||
re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_page),
|
# author
|
||||||
re_path(r'^tag/(?P<tag_id>.+)\.json/?$', views.tag_page),
|
re_path(r'^author/(?P<author_id>\d+)(.json)?/?$', views.Author.as_view()),
|
||||||
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),
|
re_path(r'^author/(?P<author_id>\d+)/edit/?$', views.EditAuthor.as_view()),
|
||||||
|
|
||||||
|
# tags
|
||||||
|
re_path(r'^tag/(?P<tag_id>.+)\.json/?$', views.Tag.as_view()),
|
||||||
|
re_path(r'^tag/(?P<tag_id>.+)/?$', views.Tag.as_view()),
|
||||||
|
re_path(r'^tag/?$', views.AddTag.as_view()),
|
||||||
|
re_path(r'^untag/?$', views.RemoveTag.as_view()),
|
||||||
|
|
||||||
|
# shelf
|
||||||
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \
|
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \
|
||||||
user_path, views.shelf_page),
|
user_path, views.Shelf.as_view()),
|
||||||
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \
|
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \
|
||||||
local_user_path, views.shelf_page),
|
local_user_path, views.Shelf.as_view()),
|
||||||
|
re_path(r'^create-shelf/?$', views.create_shelf),
|
||||||
|
re_path(r'^delete-shelf/(?P<shelf_id>\d+)?$', views.delete_shelf),
|
||||||
|
re_path(r'^shelve/?$', views.shelve),
|
||||||
|
re_path(r'^unshelve/?$', views.unshelve),
|
||||||
|
|
||||||
re_path(r'^search/?$', views.search),
|
# reading progress
|
||||||
|
re_path(r'^edit-readthrough/?$', views.edit_readthrough),
|
||||||
|
re_path(r'^delete-readthrough/?$', views.delete_readthrough),
|
||||||
|
re_path(r'^create-readthrough/?$', views.create_readthrough),
|
||||||
|
|
||||||
# internal action endpoints
|
re_path(r'^start-reading/(?P<book_id>\d+)/?$', views.start_reading),
|
||||||
re_path(r'^logout/?$', actions.user_logout),
|
re_path(r'^finish-reading/(?P<book_id>\d+)/?$', views.finish_reading),
|
||||||
re_path(r'^user-login/?$', actions.user_login),
|
|
||||||
re_path(r'^user-register/?$', actions.register),
|
|
||||||
re_path(r'^reset-password-request/?$', actions.password_reset_request),
|
|
||||||
re_path(r'^reset-password/?$', actions.password_reset),
|
|
||||||
re_path(r'^change-password/?$', actions.password_change),
|
|
||||||
|
|
||||||
re_path(r'^edit-profile/?$', actions.edit_profile),
|
|
||||||
|
|
||||||
re_path(r'^import-data/?$', actions.import_data),
|
|
||||||
re_path(r'^retry-import/?$', actions.retry_import),
|
|
||||||
re_path(r'^resolve-book/?$', actions.resolve_book),
|
|
||||||
re_path(r'^edit-book/(?P<book_id>\d+)/?$', actions.edit_book),
|
|
||||||
re_path(r'^upload-cover/(?P<book_id>\d+)/?$', actions.upload_cover),
|
|
||||||
re_path(r'^add-description/(?P<book_id>\d+)/?$', actions.add_description),
|
|
||||||
re_path(r'^edit-author/(?P<author_id>\d+)/?$', actions.edit_author),
|
|
||||||
|
|
||||||
re_path(r'^switch-edition/?$', actions.switch_edition),
|
|
||||||
re_path(r'^edit-readthrough/?$', actions.edit_readthrough),
|
|
||||||
re_path(r'^delete-readthrough/?$', actions.delete_readthrough),
|
|
||||||
re_path(r'^create-readthrough/?$', actions.create_readthrough),
|
|
||||||
|
|
||||||
re_path(r'^rate/?$', actions.rate),
|
|
||||||
re_path(r'^review/?$', actions.review),
|
|
||||||
re_path(r'^quote/?$', actions.quotate),
|
|
||||||
re_path(r'^comment/?$', actions.comment),
|
|
||||||
re_path(r'^tag/?$', actions.tag),
|
|
||||||
re_path(r'^untag/?$', actions.untag),
|
|
||||||
re_path(r'^reply/?$', actions.reply),
|
|
||||||
|
|
||||||
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
|
|
||||||
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
|
|
||||||
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
|
|
||||||
re_path(r'^unboost/(?P<status_id>\d+)/?$', actions.unboost),
|
|
||||||
|
|
||||||
re_path(r'^delete-status/(?P<status_id>\d+)/?$', actions.delete_status),
|
|
||||||
|
|
||||||
re_path(r'^create-shelf/?$', actions.create_shelf),
|
|
||||||
re_path(r'^edit-shelf/(?P<shelf_id>\d+)?$', actions.edit_shelf),
|
|
||||||
re_path(r'^delete-shelf/(?P<shelf_id>\d+)?$', actions.delete_shelf),
|
|
||||||
re_path(r'^shelve/?$', actions.shelve),
|
|
||||||
re_path(r'^unshelve/?$', actions.unshelve),
|
|
||||||
re_path(r'^start-reading/(?P<book_id>\d+)/?$', actions.start_reading),
|
|
||||||
re_path(r'^finish-reading/(?P<book_id>\d+)/?$', actions.finish_reading),
|
|
||||||
|
|
||||||
re_path(r'^follow/?$', actions.follow),
|
|
||||||
re_path(r'^unfollow/?$', actions.unfollow),
|
|
||||||
re_path(r'^accept-follow-request/?$', actions.accept_follow_request),
|
|
||||||
re_path(r'^delete-follow-request/?$', actions.delete_follow_request),
|
|
||||||
|
|
||||||
re_path(r'^clear-notifications/?$', actions.clear_notifications),
|
|
||||||
|
|
||||||
re_path(r'^create-invite/?$', actions.create_invite),
|
|
||||||
|
|
||||||
|
# following
|
||||||
|
re_path(r'^follow/?$', views.follow),
|
||||||
|
re_path(r'^unfollow/?$', views.unfollow),
|
||||||
|
re_path(r'^accept-follow-request/?$', views.accept_follow_request),
|
||||||
|
re_path(r'^delete-follow-request/?$', views.delete_follow_request),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
|
@ -1,857 +0,0 @@
|
||||||
''' views for actions you can take in the application '''
|
|
||||||
from io import BytesIO, TextIOWrapper
|
|
||||||
from uuid import uuid4
|
|
||||||
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.db import transaction
|
|
||||||
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 forms, models, outgoing, goodreads_import
|
|
||||||
from bookwyrm.connectors import connector_manager
|
|
||||||
from bookwyrm.broadcast import broadcast
|
|
||||||
from bookwyrm.emailing import password_reset_email
|
|
||||||
from bookwyrm.settings import DOMAIN
|
|
||||||
from bookwyrm.views import get_user_from_username, get_edition
|
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
|
||||||
def user_login(request):
|
|
||||||
''' authenticate user login '''
|
|
||||||
login_form = forms.LoginForm(request.POST)
|
|
||||||
|
|
||||||
localname = login_form.data['localname']
|
|
||||||
username = '%s@%s' % (localname, DOMAIN)
|
|
||||||
password = login_form.data['password']
|
|
||||||
user = authenticate(request, username=username, password=password)
|
|
||||||
if user is not None:
|
|
||||||
# successful login
|
|
||||||
login(request, user)
|
|
||||||
user.last_active_date = timezone.now()
|
|
||||||
return redirect(request.GET.get('next', '/'))
|
|
||||||
|
|
||||||
login_form.non_field_errors = 'Username or password are incorrect'
|
|
||||||
register_form = forms.RegisterForm()
|
|
||||||
data = {
|
|
||||||
'login_form': login_form,
|
|
||||||
'register_form': register_form
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'login.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
|
||||||
def register(request):
|
|
||||||
''' join the server '''
|
|
||||||
if not models.SiteSettings.get().allow_registration:
|
|
||||||
invite_code = request.POST.get('invite_code')
|
|
||||||
|
|
||||||
if not invite_code:
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
invite = get_object_or_404(models.SiteInvite, code=invite_code)
|
|
||||||
if not invite.valid():
|
|
||||||
raise PermissionDenied
|
|
||||||
else:
|
|
||||||
invite = None
|
|
||||||
|
|
||||||
form = forms.RegisterForm(request.POST)
|
|
||||||
errors = False
|
|
||||||
if not form.is_valid():
|
|
||||||
errors = True
|
|
||||||
|
|
||||||
localname = form.data['localname'].strip()
|
|
||||||
email = form.data['email']
|
|
||||||
password = form.data['password']
|
|
||||||
|
|
||||||
# check localname and email uniqueness
|
|
||||||
if models.User.objects.filter(localname=localname).first():
|
|
||||||
form.errors['localname'] = ['User with this username already exists']
|
|
||||||
errors = True
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
data = {
|
|
||||||
'login_form': forms.LoginForm(),
|
|
||||||
'register_form': form,
|
|
||||||
'invite': invite,
|
|
||||||
'valid': invite.valid() if invite else True,
|
|
||||||
}
|
|
||||||
if invite:
|
|
||||||
return TemplateResponse(request, 'invite.html', data)
|
|
||||||
return TemplateResponse(request, 'login.html', data)
|
|
||||||
|
|
||||||
username = '%s@%s' % (localname, DOMAIN)
|
|
||||||
user = models.User.objects.create_user(
|
|
||||||
username, email, password, localname=localname, local=True)
|
|
||||||
if invite:
|
|
||||||
invite.times_used += 1
|
|
||||||
invite.save()
|
|
||||||
|
|
||||||
login(request, user)
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
|
|
||||||
@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')
|
|
||||||
try:
|
|
||||||
user = models.User.objects.get(email=email)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return redirect('/password-reset')
|
|
||||||
|
|
||||||
# remove any existing password reset cods for this user
|
|
||||||
models.PasswordReset.objects.filter(user=user).all().delete()
|
|
||||||
|
|
||||||
# create a new reset code
|
|
||||||
code = models.PasswordReset.objects.create(user=user)
|
|
||||||
password_reset_email(code)
|
|
||||||
data = {'message': 'Password reset link sent to %s' % email}
|
|
||||||
return TemplateResponse(request, 'password_reset_request.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
|
||||||
def password_reset(request):
|
|
||||||
''' allow a user to change their password through an emailed token '''
|
|
||||||
try:
|
|
||||||
reset_code = models.PasswordReset.objects.get(
|
|
||||||
code=request.POST.get('reset-code')
|
|
||||||
)
|
|
||||||
except models.PasswordReset.DoesNotExist:
|
|
||||||
data = {'errors': ['Invalid password reset link']}
|
|
||||||
return TemplateResponse(request, 'password_reset.html', data)
|
|
||||||
|
|
||||||
user = reset_code.user
|
|
||||||
|
|
||||||
new_password = request.POST.get('password')
|
|
||||||
confirm_password = request.POST.get('confirm-password')
|
|
||||||
|
|
||||||
if new_password != confirm_password:
|
|
||||||
data = {'errors': ['Passwords do not match']}
|
|
||||||
return TemplateResponse(request, 'password_reset.html', data)
|
|
||||||
|
|
||||||
user.set_password(new_password)
|
|
||||||
user.save()
|
|
||||||
login(request, user)
|
|
||||||
reset_code.delete()
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def password_change(request):
|
|
||||||
''' allow a user to change their password '''
|
|
||||||
new_password = request.POST.get('password')
|
|
||||||
confirm_password = request.POST.get('confirm-password')
|
|
||||||
|
|
||||||
if new_password != confirm_password:
|
|
||||||
return redirect('/user-edit')
|
|
||||||
|
|
||||||
request.user.set_password(new_password)
|
|
||||||
request.user.save()
|
|
||||||
login(request, request.user)
|
|
||||||
return redirect('/user/%s' % request.user.localname)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def edit_profile(request):
|
|
||||||
''' les get fancy with images '''
|
|
||||||
form = forms.EditUserForm(
|
|
||||||
request.POST, request.FILES, instance=request.user)
|
|
||||||
if not form.is_valid():
|
|
||||||
data = {'form': form, 'user': request.user}
|
|
||||||
return TemplateResponse(request, 'edit_user.html', data)
|
|
||||||
|
|
||||||
user = form.save(commit=False)
|
|
||||||
|
|
||||||
if 'avatar' in form.files:
|
|
||||||
# crop and resize avatar upload
|
|
||||||
image = Image.open(form.files['avatar'])
|
|
||||||
target_size = 120
|
|
||||||
width, height = image.size
|
|
||||||
thumbnail_scale = height / (width / target_size) if height > width \
|
|
||||||
else width / (height / target_size)
|
|
||||||
image.thumbnail([thumbnail_scale, thumbnail_scale])
|
|
||||||
width, height = image.size
|
|
||||||
|
|
||||||
width_diff = width - target_size
|
|
||||||
height_diff = height - target_size
|
|
||||||
cropped = image.crop((
|
|
||||||
int(width_diff / 2),
|
|
||||||
int(height_diff / 2),
|
|
||||||
int(width - (width_diff / 2)),
|
|
||||||
int(height - (height_diff / 2))
|
|
||||||
))
|
|
||||||
output = BytesIO()
|
|
||||||
cropped.save(output, format=image.format)
|
|
||||||
ContentFile(output.getvalue())
|
|
||||||
|
|
||||||
# set the name to a hash
|
|
||||||
extension = form.files['avatar'].name.split('.')[-1]
|
|
||||||
filename = '%s.%s' % (uuid4(), extension)
|
|
||||||
user.avatar.save(filename, ContentFile(output.getvalue()))
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
broadcast(user, user.to_update_activity(user))
|
|
||||||
return redirect('/user/%s' % request.user.localname)
|
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
|
||||||
def resolve_book(request):
|
|
||||||
''' figure out the local path to a book from a remote_id '''
|
|
||||||
remote_id = request.POST.get('remote_id')
|
|
||||||
connector = connector_manager.get_or_create_connector(remote_id)
|
|
||||||
book = connector.get_or_create_book(remote_id)
|
|
||||||
|
|
||||||
return redirect('/book/%d' % book.id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@permission_required('bookwyrm.edit_book', raise_exception=True)
|
|
||||||
@require_POST
|
|
||||||
def edit_book(request, book_id):
|
|
||||||
''' edit a book cool '''
|
|
||||||
book = get_object_or_404(models.Edition, id=book_id)
|
|
||||||
|
|
||||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
|
||||||
if not form.is_valid():
|
|
||||||
data = {
|
|
||||||
'title': 'Edit Book',
|
|
||||||
'book': book,
|
|
||||||
'form': form
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'edit_book.html', data)
|
|
||||||
book = form.save()
|
|
||||||
|
|
||||||
broadcast(request.user, book.to_update_activity(request.user))
|
|
||||||
return redirect('/book/%s' % book.id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
@transaction.atomic
|
|
||||||
def switch_edition(request):
|
|
||||||
''' switch your copy of a book to a different edition '''
|
|
||||||
edition_id = request.POST.get('edition')
|
|
||||||
new_edition = get_object_or_404(models.Edition, id=edition_id)
|
|
||||||
shelfbooks = models.ShelfBook.objects.filter(
|
|
||||||
book__parent_work=new_edition.parent_work,
|
|
||||||
shelf__user=request.user
|
|
||||||
)
|
|
||||||
for shelfbook in shelfbooks.all():
|
|
||||||
broadcast(request.user, shelfbook.to_remove_activity(request.user))
|
|
||||||
|
|
||||||
shelfbook.book = new_edition
|
|
||||||
shelfbook.save()
|
|
||||||
|
|
||||||
broadcast(request.user, shelfbook.to_add_activity(request.user))
|
|
||||||
|
|
||||||
readthroughs = models.ReadThrough.objects.filter(
|
|
||||||
book__parent_work=new_edition.parent_work,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
for readthrough in readthroughs.all():
|
|
||||||
readthrough.book = new_edition
|
|
||||||
readthrough.save()
|
|
||||||
|
|
||||||
return redirect('/book/%d' % new_edition.id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def upload_cover(request, book_id):
|
|
||||||
''' upload a new cover '''
|
|
||||||
book = get_object_or_404(models.Edition, id=book_id)
|
|
||||||
|
|
||||||
form = forms.CoverForm(request.POST, request.FILES, instance=book)
|
|
||||||
if not form.is_valid():
|
|
||||||
return redirect('/book/%d' % book.id)
|
|
||||||
|
|
||||||
book.cover = form.files['cover']
|
|
||||||
book.save()
|
|
||||||
|
|
||||||
broadcast(request.user, book.to_update_activity(request.user))
|
|
||||||
return redirect('/book/%s' % book.id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
@permission_required('bookwyrm.edit_book', raise_exception=True)
|
|
||||||
def add_description(request, book_id):
|
|
||||||
''' upload a new cover '''
|
|
||||||
if not request.method == 'POST':
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
book = get_object_or_404(models.Edition, id=book_id)
|
|
||||||
|
|
||||||
description = request.POST.get('description')
|
|
||||||
|
|
||||||
book.description = description
|
|
||||||
book.save()
|
|
||||||
|
|
||||||
broadcast(request.user, book.to_update_activity(request.user))
|
|
||||||
return redirect('/book/%s' % book.id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@permission_required('bookwyrm.edit_book', raise_exception=True)
|
|
||||||
@require_POST
|
|
||||||
def edit_author(request, author_id):
|
|
||||||
''' edit a author cool '''
|
|
||||||
author = get_object_or_404(models.Author, id=author_id)
|
|
||||||
|
|
||||||
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
|
|
||||||
if not form.is_valid():
|
|
||||||
data = {
|
|
||||||
'title': 'Edit Author',
|
|
||||||
'author': author,
|
|
||||||
'form': form
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'edit_author.html', data)
|
|
||||||
author = form.save()
|
|
||||||
|
|
||||||
broadcast(request.user, author.to_update_activity(request.user))
|
|
||||||
return redirect('/author/%s' % author.id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def create_shelf(request):
|
|
||||||
''' user generated shelves '''
|
|
||||||
form = forms.ShelfForm(request.POST)
|
|
||||||
if not form.is_valid():
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
shelf = form.save()
|
|
||||||
return redirect('/user/%s/shelf/%s' % \
|
|
||||||
(request.user.localname, shelf.identifier))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def edit_shelf(request, shelf_id):
|
|
||||||
''' user generated shelves '''
|
|
||||||
shelf = get_object_or_404(models.Shelf, id=shelf_id)
|
|
||||||
if request.user != shelf.user:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
if not shelf.editable and request.POST.get('name') != shelf.name:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
form = forms.ShelfForm(request.POST, instance=shelf)
|
|
||||||
if not form.is_valid():
|
|
||||||
return redirect(shelf.local_path)
|
|
||||||
shelf = form.save()
|
|
||||||
return redirect(shelf.local_path)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def delete_shelf(request, shelf_id):
|
|
||||||
''' user generated shelves '''
|
|
||||||
shelf = get_object_or_404(models.Shelf, id=shelf_id)
|
|
||||||
if request.user != shelf.user or not shelf.editable:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
shelf.delete()
|
|
||||||
return redirect('/user/%s/shelves' % request.user.localname)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def shelve(request):
|
|
||||||
''' put a on a user's shelf '''
|
|
||||||
book = get_edition(request.POST['book'])
|
|
||||||
|
|
||||||
desired_shelf = models.Shelf.objects.filter(
|
|
||||||
identifier=request.POST['shelf'],
|
|
||||||
user=request.user
|
|
||||||
).first()
|
|
||||||
|
|
||||||
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, desired_shelf)
|
|
||||||
|
|
||||||
# post about "want to read" shelves
|
|
||||||
if desired_shelf.identifier == 'to-read':
|
|
||||||
outgoing.handle_reading_status(
|
|
||||||
request.user,
|
|
||||||
desired_shelf,
|
|
||||||
book,
|
|
||||||
privacy=desired_shelf.privacy
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def unshelve(request):
|
|
||||||
''' put a on a user's shelf '''
|
|
||||||
book = models.Edition.objects.get(id=request.POST['book'])
|
|
||||||
current_shelf = models.Shelf.objects.get(id=request.POST['shelf'])
|
|
||||||
|
|
||||||
outgoing.handle_unshelve(request.user, book, current_shelf)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def start_reading(request, book_id):
|
|
||||||
''' begin reading a book '''
|
|
||||||
book = get_edition(book_id)
|
|
||||||
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
|
|
||||||
@require_POST
|
|
||||||
def finish_reading(request, book_id):
|
|
||||||
''' a user completed a book, yay '''
|
|
||||||
book = get_edition(book_id)
|
|
||||||
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
|
|
||||||
@require_POST
|
|
||||||
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
|
|
||||||
@require_POST
|
|
||||||
def delete_readthrough(request):
|
|
||||||
''' remove a readthrough '''
|
|
||||||
readthrough = get_object_or_404(
|
|
||||||
models.ReadThrough, id=request.POST.get('id'))
|
|
||||||
|
|
||||||
# don't let people edit other people's data
|
|
||||||
if request.user != readthrough.user:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
readthrough.delete()
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def create_readthrough(request):
|
|
||||||
''' can't use the form because the dates are too finnicky '''
|
|
||||||
book = get_object_or_404(models.Edition, id=request.POST.get('book'))
|
|
||||||
readthrough = update_readthrough(request, create=True, book=book)
|
|
||||||
if not readthrough:
|
|
||||||
return redirect(book.local_path)
|
|
||||||
readthrough.save()
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def rate(request):
|
|
||||||
''' just a star rating for a book '''
|
|
||||||
form = forms.RatingForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def review(request):
|
|
||||||
''' create a book review '''
|
|
||||||
form = forms.ReviewForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def quotate(request):
|
|
||||||
''' create a book quotation '''
|
|
||||||
form = forms.QuotationForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def comment(request):
|
|
||||||
''' create a book comment '''
|
|
||||||
form = forms.CommentForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def reply(request):
|
|
||||||
''' respond to a book review '''
|
|
||||||
form = forms.ReplyForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_status(request, form):
|
|
||||||
''' all the "create a status" functions are the same '''
|
|
||||||
if not form.is_valid():
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
outgoing.handle_status(request.user, form)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
# field which doesn't validate
|
|
||||||
name = request.POST.get('name')
|
|
||||||
book_id = request.POST.get('book')
|
|
||||||
book = get_object_or_404(models.Edition, id=book_id)
|
|
||||||
tag_obj, created = models.Tag.objects.get_or_create(
|
|
||||||
name=name,
|
|
||||||
)
|
|
||||||
user_tag, _ = models.UserTag.objects.get_or_create(
|
|
||||||
user=request.user,
|
|
||||||
book=book,
|
|
||||||
tag=tag_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
if created:
|
|
||||||
broadcast(request.user, user_tag.to_add_activity(request.user))
|
|
||||||
return redirect('/book/%s' % book_id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def untag(request):
|
|
||||||
''' untag a book '''
|
|
||||||
name = request.POST.get('name')
|
|
||||||
tag_obj = get_object_or_404(models.Tag, name=name)
|
|
||||||
book_id = request.POST.get('book')
|
|
||||||
book = get_object_or_404(models.Edition, id=book_id)
|
|
||||||
|
|
||||||
user_tag = get_object_or_404(
|
|
||||||
models.UserTag, tag=tag_obj, book=book, user=request.user)
|
|
||||||
tag_activity = user_tag.to_remove_activity(request.user)
|
|
||||||
user_tag.delete()
|
|
||||||
|
|
||||||
broadcast(request.user, tag_activity)
|
|
||||||
return redirect('/book/%s' % book_id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def favorite(request, status_id):
|
|
||||||
''' like a status '''
|
|
||||||
status = models.Status.objects.get(id=status_id)
|
|
||||||
outgoing.handle_favorite(request.user, status)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def unfavorite(request, status_id):
|
|
||||||
''' like a status '''
|
|
||||||
status = models.Status.objects.get(id=status_id)
|
|
||||||
outgoing.handle_unfavorite(request.user, status)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def boost(request, status_id):
|
|
||||||
''' boost a status '''
|
|
||||||
status = models.Status.objects.get(id=status_id)
|
|
||||||
outgoing.handle_boost(request.user, status)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def unboost(request, status_id):
|
|
||||||
''' boost a status '''
|
|
||||||
status = models.Status.objects.get(id=status_id)
|
|
||||||
outgoing.handle_unboost(request.user, status)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
@require_POST
|
|
||||||
def follow(request):
|
|
||||||
''' follow another user, here or abroad '''
|
|
||||||
username = request.POST['user']
|
|
||||||
try:
|
|
||||||
to_follow = get_user_from_username(username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
outgoing.handle_follow(request.user, to_follow)
|
|
||||||
user_slug = to_follow.localname if to_follow.localname \
|
|
||||||
else to_follow.username
|
|
||||||
return redirect('/user/%s' % user_slug)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def unfollow(request):
|
|
||||||
''' unfollow a user '''
|
|
||||||
username = request.POST['user']
|
|
||||||
try:
|
|
||||||
to_unfollow = get_user_from_username(username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
outgoing.handle_unfollow(request.user, to_unfollow)
|
|
||||||
user_slug = to_unfollow.localname if to_unfollow.localname \
|
|
||||||
else to_unfollow.username
|
|
||||||
return redirect('/user/%s' % user_slug)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def clear_notifications(request):
|
|
||||||
''' permanently delete notification for user '''
|
|
||||||
request.user.notification_set.filter(read=True).delete()
|
|
||||||
return redirect('/notifications')
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def accept_follow_request(request):
|
|
||||||
''' a user accepts a follow request '''
|
|
||||||
username = request.POST['user']
|
|
||||||
try:
|
|
||||||
requester = get_user_from_username(username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
try:
|
|
||||||
follow_request = models.UserFollowRequest.objects.get(
|
|
||||||
user_subject=requester,
|
|
||||||
user_object=request.user
|
|
||||||
)
|
|
||||||
except models.UserFollowRequest.DoesNotExist:
|
|
||||||
# Request already dealt with.
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
outgoing.handle_accept(follow_request)
|
|
||||||
|
|
||||||
return redirect('/user/%s' % request.user.localname)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def delete_follow_request(request):
|
|
||||||
''' a user rejects a follow request '''
|
|
||||||
username = request.POST['user']
|
|
||||||
try:
|
|
||||||
requester = get_user_from_username(username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
try:
|
|
||||||
follow_request = models.UserFollowRequest.objects.get(
|
|
||||||
user_subject=requester,
|
|
||||||
user_object=request.user
|
|
||||||
)
|
|
||||||
except models.UserFollowRequest.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
outgoing.handle_reject(follow_request)
|
|
||||||
return redirect('/user/%s' % request.user.localname)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
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),
|
|
||||||
include_reviews,
|
|
||||||
privacy,
|
|
||||||
)
|
|
||||||
except (UnicodeDecodeError, ValueError):
|
|
||||||
return HttpResponseBadRequest('Not a valid csv file')
|
|
||||||
goodreads_import.start_import(job)
|
|
||||||
return redirect('/import-status/%d' % job.id)
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
|
|
||||||
@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'))
|
|
||||||
items = []
|
|
||||||
for item in request.POST.getlist('import_item'):
|
|
||||||
items.append(get_object_or_404(models.ImportItem, id=item))
|
|
||||||
|
|
||||||
job = goodreads_import.create_retry_job(
|
|
||||||
request.user,
|
|
||||||
job,
|
|
||||||
items,
|
|
||||||
)
|
|
||||||
goodreads_import.start_import(job)
|
|
||||||
return redirect('/import-status/%d' % job.id)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
@permission_required('bookwyrm.create_invites', raise_exception=True)
|
|
||||||
def create_invite(request):
|
|
||||||
''' creates a user invite database entry '''
|
|
||||||
form = forms.CreateInviteForm(request.POST)
|
|
||||||
if not form.is_valid():
|
|
||||||
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
|
|
||||||
|
|
||||||
invite = form.save(commit=False)
|
|
||||||
invite.user = request.user
|
|
||||||
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 = timezone.make_aware(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 = timezone.make_aware(
|
|
||||||
dateutil.parser.parse(finish_date))
|
|
||||||
readthrough.finish_date = finish_date
|
|
||||||
except ParserError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not readthrough.start_date and not readthrough.finish_date:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return readthrough
|
|
|
@ -1,837 +0,0 @@
|
||||||
''' views for pages you can go to in the application '''
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
|
||||||
from django.contrib.postgres.search import TrigramSimilarity
|
|
||||||
from django.core.paginator import Paginator
|
|
||||||
from django.db.models import Avg, Q, Max
|
|
||||||
from django.db.models.functions import Greatest
|
|
||||||
from django.http import HttpResponseNotFound, JsonResponse
|
|
||||||
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 import forms, models
|
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
|
||||||
from bookwyrm.connectors import connector_manager
|
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
|
||||||
from bookwyrm.tasks import app
|
|
||||||
from bookwyrm.utils import regex
|
|
||||||
|
|
||||||
|
|
||||||
def get_edition(book_id):
|
|
||||||
''' look up a book in the db and return an edition '''
|
|
||||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
|
||||||
if isinstance(book, models.Work):
|
|
||||||
book = book.get_default_edition()
|
|
||||||
return book
|
|
||||||
|
|
||||||
def get_user_from_username(username):
|
|
||||||
''' helper function to resolve a localname or a username to a user '''
|
|
||||||
# raises DoesNotExist if user is now found
|
|
||||||
try:
|
|
||||||
return models.User.objects.get(localname=username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return models.User.objects.get(username=username)
|
|
||||||
|
|
||||||
|
|
||||||
def is_api_request(request):
|
|
||||||
''' check whether a request is asking for html or data '''
|
|
||||||
return 'json' in request.headers.get('Accept') or \
|
|
||||||
request.path[-5:] == '.json'
|
|
||||||
|
|
||||||
def is_bookworm_request(request):
|
|
||||||
''' check if the request is coming from another bookworm instance '''
|
|
||||||
user_agent = request.headers.get('User-Agent')
|
|
||||||
if user_agent is None or \
|
|
||||||
re.search(regex.bookwyrm_user_agent, user_agent) is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def server_error_page(request):
|
|
||||||
''' 500 errors '''
|
|
||||||
return TemplateResponse(
|
|
||||||
request, 'error.html', {'title': 'Oops!'}, status=500)
|
|
||||||
|
|
||||||
|
|
||||||
def not_found_page(request, _):
|
|
||||||
''' 404s '''
|
|
||||||
return TemplateResponse(
|
|
||||||
request, 'notfound.html', {'title': 'Not found'}, status=404)
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def home(request):
|
|
||||||
''' this is the same as the feed on the home tab '''
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
return home_tab(request, 'home')
|
|
||||||
return discover_page(request)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_GET
|
|
||||||
def home_tab(request, tab):
|
|
||||||
''' user's homepage with activity feed '''
|
|
||||||
try:
|
|
||||||
page = int(request.GET.get('page', 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
suggested_books = get_suggested_books(request.user)
|
|
||||||
|
|
||||||
if tab == 'home':
|
|
||||||
activities = get_activity_feed(
|
|
||||||
request.user, ['public', 'unlisted', 'followers'],
|
|
||||||
following_only=True)
|
|
||||||
elif tab == 'local':
|
|
||||||
activities = get_activity_feed(
|
|
||||||
request.user, ['public', 'followers'], local_only=True)
|
|
||||||
else:
|
|
||||||
activities = get_activity_feed(
|
|
||||||
request.user, ['public', 'followers'])
|
|
||||||
paginated = Paginator(activities, PAGE_LENGTH)
|
|
||||||
activity_page = paginated.page(page)
|
|
||||||
|
|
||||||
prev_page = next_page = None
|
|
||||||
if activity_page.has_next():
|
|
||||||
next_page = '/%s/?page=%d#feed' % \
|
|
||||||
(tab, activity_page.next_page_number())
|
|
||||||
if activity_page.has_previous():
|
|
||||||
prev_page = '/%s/?page=%d#feed' % \
|
|
||||||
(tab, activity_page.previous_page_number())
|
|
||||||
data = {
|
|
||||||
'title': 'Updates Feed',
|
|
||||||
'user': request.user,
|
|
||||||
'suggested_books': suggested_books,
|
|
||||||
'activities': activity_page.object_list,
|
|
||||||
'tab': tab,
|
|
||||||
'next': next_page,
|
|
||||||
'prev': prev_page,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'feed.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
def get_suggested_books(user, max_books=5):
|
|
||||||
''' helper to get a user's recent books '''
|
|
||||||
book_count = 0
|
|
||||||
preset_shelves = [
|
|
||||||
('reading', max_books), ('read', 2), ('to-read', max_books)
|
|
||||||
]
|
|
||||||
suggested_books = []
|
|
||||||
for (preset, shelf_max) in preset_shelves:
|
|
||||||
limit = shelf_max if shelf_max < (max_books - book_count) \
|
|
||||||
else max_books - book_count
|
|
||||||
shelf = user.shelf_set.get(identifier=preset)
|
|
||||||
|
|
||||||
shelf_books = shelf.shelfbook_set.order_by(
|
|
||||||
'-updated_date'
|
|
||||||
).all()[:limit]
|
|
||||||
if not shelf_books:
|
|
||||||
continue
|
|
||||||
shelf_preview = {
|
|
||||||
'name': shelf.name,
|
|
||||||
'books': [s.book for s in shelf_books]
|
|
||||||
}
|
|
||||||
suggested_books.append(shelf_preview)
|
|
||||||
book_count += len(shelf_preview['books'])
|
|
||||||
return suggested_books
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def discover_page(request):
|
|
||||||
''' tiled book activity page '''
|
|
||||||
books = models.Edition.objects.filter(
|
|
||||||
review__published_date__isnull=False,
|
|
||||||
review__user__local=True,
|
|
||||||
review__privacy__in=['public', 'unlisted'],
|
|
||||||
).exclude(
|
|
||||||
cover__exact=''
|
|
||||||
).annotate(
|
|
||||||
Max('review__published_date')
|
|
||||||
).order_by('-review__published_date__max')[:6]
|
|
||||||
|
|
||||||
ratings = {}
|
|
||||||
for book in books:
|
|
||||||
reviews = models.Review.objects.filter(
|
|
||||||
book__in=book.parent_work.editions.all()
|
|
||||||
)
|
|
||||||
reviews = get_activity_feed(
|
|
||||||
request.user, ['public', 'unlisted'], queryset=reviews)
|
|
||||||
ratings[book.id] = reviews.aggregate(Avg('rating'))['rating__avg']
|
|
||||||
data = {
|
|
||||||
'title': 'Discover',
|
|
||||||
'register_form': forms.RegisterForm(),
|
|
||||||
'books': list(set(books)),
|
|
||||||
'ratings': ratings
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'discover.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_GET
|
|
||||||
def direct_messages_page(request, page=1):
|
|
||||||
''' like a feed but for dms only '''
|
|
||||||
activities = get_activity_feed(request.user, 'direct')
|
|
||||||
paginated = Paginator(activities, PAGE_LENGTH)
|
|
||||||
activity_page = paginated.page(page)
|
|
||||||
|
|
||||||
prev_page = next_page = None
|
|
||||||
if activity_page.has_next():
|
|
||||||
next_page = '/direct-message/?page=%d#feed' % \
|
|
||||||
activity_page.next_page_number()
|
|
||||||
if activity_page.has_previous():
|
|
||||||
prev_page = '/direct-messages/?page=%d#feed' % \
|
|
||||||
activity_page.previous_page_number()
|
|
||||||
data = {
|
|
||||||
'title': 'Direct Messages',
|
|
||||||
'user': request.user,
|
|
||||||
'activities': activity_page.object_list,
|
|
||||||
'next': next_page,
|
|
||||||
'prev': prev_page,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'direct_messages.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
def get_activity_feed(
|
|
||||||
user, privacy, local_only=False, following_only=False,
|
|
||||||
queryset=models.Status.objects):
|
|
||||||
''' get a filtered queryset of statuses '''
|
|
||||||
privacy = privacy if isinstance(privacy, list) else [privacy]
|
|
||||||
# if we're looking at Status, we need this. We don't if it's Comment
|
|
||||||
if hasattr(queryset, 'select_subclasses'):
|
|
||||||
queryset = queryset.select_subclasses()
|
|
||||||
|
|
||||||
# exclude deleted
|
|
||||||
queryset = queryset.exclude(deleted=True).order_by('-published_date')
|
|
||||||
|
|
||||||
# you can't see followers only or direct messages if you're not logged in
|
|
||||||
if user.is_anonymous:
|
|
||||||
privacy = [p for p in privacy if not p in ['followers', 'direct']]
|
|
||||||
|
|
||||||
# filter to only privided privacy levels
|
|
||||||
queryset = queryset.filter(privacy__in=privacy)
|
|
||||||
|
|
||||||
# only include statuses the user follows
|
|
||||||
if following_only:
|
|
||||||
queryset = queryset.exclude(
|
|
||||||
~Q(# remove everythign except
|
|
||||||
Q(user__in=user.following.all()) | # user follwoing
|
|
||||||
Q(user=user) |# is self
|
|
||||||
Q(mention_users=user)# mentions user
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# exclude followers-only statuses the user doesn't follow
|
|
||||||
elif 'followers' in privacy:
|
|
||||||
queryset = queryset.exclude(
|
|
||||||
~Q(# user isn't following and it isn't their own status
|
|
||||||
Q(user__in=user.following.all()) | Q(user=user)
|
|
||||||
),
|
|
||||||
privacy='followers' # and the status is followers only
|
|
||||||
)
|
|
||||||
|
|
||||||
# exclude direct messages not intended for the user
|
|
||||||
if 'direct' in privacy:
|
|
||||||
queryset = queryset.exclude(
|
|
||||||
~Q(
|
|
||||||
Q(user=user) | Q(mention_users=user)
|
|
||||||
), privacy='direct'
|
|
||||||
)
|
|
||||||
|
|
||||||
# filter for only local status
|
|
||||||
if local_only:
|
|
||||||
queryset = queryset.filter(user__local=True)
|
|
||||||
|
|
||||||
# remove statuses that have boosts in the same queryset
|
|
||||||
try:
|
|
||||||
queryset = queryset.filter(~Q(boosters__in=queryset))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def search(request):
|
|
||||||
''' that search bar up top '''
|
|
||||||
query = request.GET.get('q')
|
|
||||||
min_confidence = request.GET.get('min_confidence', 0.1)
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
# only return local book results via json so we don't cause a cascade
|
|
||||||
book_results = connector_manager.local_search(
|
|
||||||
query, min_confidence=min_confidence)
|
|
||||||
return JsonResponse([r.json() for r in book_results], safe=False)
|
|
||||||
|
|
||||||
# use webfinger for mastodon style account@domain.com username
|
|
||||||
if re.match(r'\B%s' % regex.full_username, query):
|
|
||||||
outgoing.handle_remote_webfinger(query)
|
|
||||||
|
|
||||||
# do a local user search
|
|
||||||
user_results = models.User.objects.annotate(
|
|
||||||
similarity=Greatest(
|
|
||||||
TrigramSimilarity('username', query),
|
|
||||||
TrigramSimilarity('localname', query),
|
|
||||||
)
|
|
||||||
).filter(
|
|
||||||
similarity__gt=0.5,
|
|
||||||
).order_by('-similarity')[:10]
|
|
||||||
|
|
||||||
book_results = connector_manager.search(
|
|
||||||
query, min_confidence=min_confidence)
|
|
||||||
data = {
|
|
||||||
'title': 'Search Results',
|
|
||||||
'book_results': book_results,
|
|
||||||
'user_results': user_results,
|
|
||||||
'query': query,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'search_results.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_GET
|
|
||||||
def import_page(request):
|
|
||||||
''' import history from goodreads '''
|
|
||||||
return TemplateResponse(request, 'import.html', {
|
|
||||||
'title': 'Import Books',
|
|
||||||
'import_form': forms.ImportForm(),
|
|
||||||
'jobs': models.ImportJob.
|
|
||||||
objects.filter(user=request.user).order_by('-created_date'),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_GET
|
|
||||||
def import_status(request, job_id):
|
|
||||||
''' status of an import job '''
|
|
||||||
job = models.ImportJob.objects.get(id=job_id)
|
|
||||||
if job.user != request.user:
|
|
||||||
raise PermissionDenied
|
|
||||||
task = app.AsyncResult(job.task_id)
|
|
||||||
items = job.items.order_by('index').all()
|
|
||||||
failed_items = [i for i in items if i.fail_reason]
|
|
||||||
items = [i for i in items if not i.fail_reason]
|
|
||||||
return TemplateResponse(request, 'import_status.html', {
|
|
||||||
'title': 'Import Status',
|
|
||||||
'job': job,
|
|
||||||
'items': items,
|
|
||||||
'failed_items': failed_items,
|
|
||||||
'task': task
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def login_page(request):
|
|
||||||
''' authentication '''
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
return redirect('/')
|
|
||||||
# send user to the login page
|
|
||||||
data = {
|
|
||||||
'title': 'Login',
|
|
||||||
'login_form': forms.LoginForm(),
|
|
||||||
'register_form': forms.RegisterForm(),
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'login.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def about_page(request):
|
|
||||||
''' more information about the instance '''
|
|
||||||
data = {
|
|
||||||
'title': 'About',
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'about.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def password_reset_request(request):
|
|
||||||
''' invite management page '''
|
|
||||||
return TemplateResponse(
|
|
||||||
request,
|
|
||||||
'password_reset_request.html',
|
|
||||||
{'title': 'Reset Password'}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def password_reset(request, code):
|
|
||||||
''' endpoint for sending invites '''
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
return redirect('/')
|
|
||||||
try:
|
|
||||||
reset_code = models.PasswordReset.objects.get(code=code)
|
|
||||||
if not reset_code.valid():
|
|
||||||
raise PermissionDenied
|
|
||||||
except models.PasswordReset.DoesNotExist:
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
return TemplateResponse(
|
|
||||||
request,
|
|
||||||
'password_reset.html',
|
|
||||||
{'title': 'Reset Password', 'code': reset_code.code}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def invite_page(request, code):
|
|
||||||
''' endpoint for sending invites '''
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
return redirect('/')
|
|
||||||
invite = get_object_or_404(models.SiteInvite, code=code)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'title': 'Join',
|
|
||||||
'register_form': forms.RegisterForm(),
|
|
||||||
'invite': invite,
|
|
||||||
'valid': invite.valid() if invite else True,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'invite.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@permission_required('bookwyrm.create_invites', raise_exception=True)
|
|
||||||
@require_GET
|
|
||||||
def manage_invites(request):
|
|
||||||
''' invite management page '''
|
|
||||||
data = {
|
|
||||||
'title': 'Invitations',
|
|
||||||
'invites': models.SiteInvite.objects.filter(
|
|
||||||
user=request.user).order_by('-created_date'),
|
|
||||||
'form': forms.CreateInviteForm(),
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'manage_invites.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_GET
|
|
||||||
def notifications_page(request):
|
|
||||||
''' list notitications '''
|
|
||||||
notifications = request.user.notification_set.all() \
|
|
||||||
.order_by('-created_date')
|
|
||||||
unread = [n.id for n in notifications.filter(read=False)]
|
|
||||||
data = {
|
|
||||||
'title': 'Notifications',
|
|
||||||
'notifications': notifications,
|
|
||||||
'unread': unread,
|
|
||||||
}
|
|
||||||
notifications.update(read=True)
|
|
||||||
return TemplateResponse(request, 'notifications.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_GET
|
|
||||||
def user_page(request, username):
|
|
||||||
''' profile page for a user '''
|
|
||||||
try:
|
|
||||||
user = get_user_from_username(username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
# we have a json request
|
|
||||||
return ActivitypubResponse(user.to_activity())
|
|
||||||
# otherwise we're at a UI view
|
|
||||||
|
|
||||||
try:
|
|
||||||
page = int(request.GET.get('page', 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
shelf_preview = []
|
|
||||||
|
|
||||||
# only show other shelves that should be visible
|
|
||||||
shelves = user.shelf_set
|
|
||||||
is_self = request.user.id == user.id
|
|
||||||
if not is_self:
|
|
||||||
follower = user.followers.filter(id=request.user.id).exists()
|
|
||||||
if follower:
|
|
||||||
shelves = shelves.filter(privacy__in=['public', 'followers'])
|
|
||||||
else:
|
|
||||||
shelves = shelves.filter(privacy='public')
|
|
||||||
|
|
||||||
for user_shelf in shelves.all():
|
|
||||||
if not user_shelf.books.count():
|
|
||||||
continue
|
|
||||||
shelf_preview.append({
|
|
||||||
'name': user_shelf.name,
|
|
||||||
'local_path': user_shelf.local_path,
|
|
||||||
'books': user_shelf.books.all()[:3],
|
|
||||||
'size': user_shelf.books.count(),
|
|
||||||
})
|
|
||||||
if len(shelf_preview) > 2:
|
|
||||||
break
|
|
||||||
|
|
||||||
# user's posts
|
|
||||||
activities = get_activity_feed(
|
|
||||||
request.user,
|
|
||||||
['public', 'unlisted', 'followers'],
|
|
||||||
queryset=models.Status.objects.filter(user=user)
|
|
||||||
)
|
|
||||||
paginated = Paginator(activities, PAGE_LENGTH)
|
|
||||||
activity_page = paginated.page(page)
|
|
||||||
|
|
||||||
prev_page = next_page = None
|
|
||||||
if activity_page.has_next():
|
|
||||||
next_page = '/user/%s/?page=%d' % \
|
|
||||||
(username, activity_page.next_page_number())
|
|
||||||
if activity_page.has_previous():
|
|
||||||
prev_page = '/user/%s/?page=%d' % \
|
|
||||||
(username, activity_page.previous_page_number())
|
|
||||||
data = {
|
|
||||||
'title': user.name,
|
|
||||||
'user': user,
|
|
||||||
'is_self': is_self,
|
|
||||||
'shelves': shelf_preview,
|
|
||||||
'shelf_count': shelves.count(),
|
|
||||||
'activities': activity_page.object_list,
|
|
||||||
'next': next_page,
|
|
||||||
'prev': prev_page,
|
|
||||||
}
|
|
||||||
|
|
||||||
return TemplateResponse(request, 'user.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_GET
|
|
||||||
def followers_page(request, username):
|
|
||||||
''' list of followers '''
|
|
||||||
try:
|
|
||||||
user = get_user_from_username(username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(user.to_followers_activity(**request.GET))
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'title': '%s: followers' % user.name,
|
|
||||||
'user': user,
|
|
||||||
'is_self': request.user.id == user.id,
|
|
||||||
'followers': user.followers.all(),
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'followers.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_GET
|
|
||||||
def following_page(request, username):
|
|
||||||
''' list of followers '''
|
|
||||||
try:
|
|
||||||
user = get_user_from_username(username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(user.to_following_activity(**request.GET))
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'title': '%s: following' % user.name,
|
|
||||||
'user': user,
|
|
||||||
'is_self': request.user.id == user.id,
|
|
||||||
'following': user.following.all(),
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'following.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_GET
|
|
||||||
def status_page(request, username, status_id):
|
|
||||||
''' display a particular status (and replies, etc) '''
|
|
||||||
try:
|
|
||||||
user = get_user_from_username(username)
|
|
||||||
status = models.Status.objects.select_subclasses().get(id=status_id)
|
|
||||||
except ValueError:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
# the url should have the poster's username in it
|
|
||||||
if user != status.user:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
# make sure the user is authorized to see the status
|
|
||||||
if not status_visible_to_user(request.user, status):
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(
|
|
||||||
status.to_activity(pure=not is_bookworm_request(request)))
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'title': 'Status by %s' % user.username,
|
|
||||||
'status': status,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'status.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
def status_visible_to_user(viewer, status):
|
|
||||||
''' is a user authorized to view a status? '''
|
|
||||||
if viewer == status.user or status.privacy in ['public', 'unlisted']:
|
|
||||||
return True
|
|
||||||
if status.privacy == 'followers' and \
|
|
||||||
status.user.followers.filter(id=viewer.id).first():
|
|
||||||
return True
|
|
||||||
if status.privacy == 'direct' and \
|
|
||||||
status.mention_users.filter(id=viewer.id).first():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_GET
|
|
||||||
def replies_page(request, username, status_id):
|
|
||||||
''' ordered collection of replies to a status '''
|
|
||||||
if not is_api_request(request):
|
|
||||||
return status_page(request, username, status_id)
|
|
||||||
|
|
||||||
status = models.Status.objects.get(id=status_id)
|
|
||||||
if status.user.localname != username:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
return ActivitypubResponse(status.to_replies(**request.GET))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_GET
|
|
||||||
def edit_profile_page(request):
|
|
||||||
''' profile page for a user '''
|
|
||||||
user = request.user
|
|
||||||
|
|
||||||
form = forms.EditUserForm(instance=request.user)
|
|
||||||
data = {
|
|
||||||
'title': 'Edit profile',
|
|
||||||
'form': form,
|
|
||||||
'user': user,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'edit_user.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def book_page(request, book_id):
|
|
||||||
''' info about a book '''
|
|
||||||
try:
|
|
||||||
page = int(request.GET.get('page', 1))
|
|
||||||
except ValueError:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
|
||||||
except models.Book.DoesNotExist:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(book.to_activity())
|
|
||||||
|
|
||||||
if isinstance(book, models.Work):
|
|
||||||
book = book.get_default_edition()
|
|
||||||
if not book:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
work = book.parent_work
|
|
||||||
if not work:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
reviews = models.Review.objects.filter(
|
|
||||||
book__in=work.editions.all(),
|
|
||||||
)
|
|
||||||
# all reviews for the book
|
|
||||||
reviews = get_activity_feed(
|
|
||||||
request.user,
|
|
||||||
['public', 'unlisted', 'followers', 'direct'],
|
|
||||||
queryset=reviews
|
|
||||||
)
|
|
||||||
|
|
||||||
# the reviews to show
|
|
||||||
paginated = Paginator(reviews.exclude(
|
|
||||||
Q(content__isnull=True) | Q(content='')
|
|
||||||
), PAGE_LENGTH)
|
|
||||||
reviews_page = paginated.page(page)
|
|
||||||
|
|
||||||
prev_page = next_page = None
|
|
||||||
if reviews_page.has_next():
|
|
||||||
next_page = '/book/%d/?page=%d' % \
|
|
||||||
(book_id, reviews_page.next_page_number())
|
|
||||||
if reviews_page.has_previous():
|
|
||||||
prev_page = '/book/%s/?page=%d' % \
|
|
||||||
(book_id, reviews_page.previous_page_number())
|
|
||||||
|
|
||||||
user_tags = readthroughs = user_shelves = other_edition_shelves = []
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
user_tags = models.UserTag.objects.filter(
|
|
||||||
book=book, user=request.user
|
|
||||||
).values_list('tag__identifier', flat=True)
|
|
||||||
|
|
||||||
readthroughs = models.ReadThrough.objects.filter(
|
|
||||||
user=request.user,
|
|
||||||
book=book,
|
|
||||||
).order_by('start_date')
|
|
||||||
|
|
||||||
user_shelves = models.ShelfBook.objects.filter(
|
|
||||||
added_by=request.user, book=book
|
|
||||||
)
|
|
||||||
|
|
||||||
other_edition_shelves = models.ShelfBook.objects.filter(
|
|
||||||
~Q(book=book),
|
|
||||||
added_by=request.user,
|
|
||||||
book__parent_work=book.parent_work,
|
|
||||||
)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'title': book.title,
|
|
||||||
'book': book,
|
|
||||||
'reviews': reviews_page,
|
|
||||||
'review_count': reviews.count(),
|
|
||||||
'ratings': reviews.filter(Q(content__isnull=True) | Q(content='')),
|
|
||||||
'rating': reviews.aggregate(Avg('rating'))['rating__avg'],
|
|
||||||
'tags': models.UserTag.objects.filter(book=book),
|
|
||||||
'user_tags': user_tags,
|
|
||||||
'user_shelves': user_shelves,
|
|
||||||
'other_edition_shelves': other_edition_shelves,
|
|
||||||
'readthroughs': readthroughs,
|
|
||||||
'path': '/book/%s' % book_id,
|
|
||||||
'next': next_page,
|
|
||||||
'prev': prev_page,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'book.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@permission_required('bookwyrm.edit_book', raise_exception=True)
|
|
||||||
@require_GET
|
|
||||||
def edit_book_page(request, book_id):
|
|
||||||
''' info about a book '''
|
|
||||||
book = get_edition(book_id)
|
|
||||||
if not book.description:
|
|
||||||
book.description = book.parent_work.description
|
|
||||||
data = {
|
|
||||||
'title': 'Edit Book',
|
|
||||||
'book': book,
|
|
||||||
'form': forms.EditionForm(instance=book)
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'edit_book.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@permission_required('bookwyrm.edit_book', raise_exception=True)
|
|
||||||
@require_GET
|
|
||||||
def edit_author_page(request, author_id):
|
|
||||||
''' info about a book '''
|
|
||||||
author = get_object_or_404(models.Author, id=author_id)
|
|
||||||
data = {
|
|
||||||
'title': 'Edit Author',
|
|
||||||
'author': author,
|
|
||||||
'form': forms.AuthorForm(instance=author)
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'edit_author.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)
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(work.to_edition_list(**request.GET))
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'title': 'Editions of %s' % work.title,
|
|
||||||
'editions': work.editions.order_by('-edition_rank').all(),
|
|
||||||
'work': work,
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(author.to_activity())
|
|
||||||
|
|
||||||
books = models.Work.objects.filter(
|
|
||||||
Q(authors=author) | Q(editions__authors=author)).distinct()
|
|
||||||
data = {
|
|
||||||
'title': author.name,
|
|
||||||
'author': author,
|
|
||||||
'books': [b.get_default_edition() for b in books],
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
if not tag_obj:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(tag_obj.to_activity(**request.GET))
|
|
||||||
|
|
||||||
books = models.Edition.objects.filter(
|
|
||||||
usertag__tag__identifier=tag_id
|
|
||||||
).distinct()
|
|
||||||
data = {
|
|
||||||
'title': tag_obj.name,
|
|
||||||
'books': books,
|
|
||||||
'tag': tag_obj,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'tag.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
@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:
|
|
||||||
user = get_user_from_username(username)
|
|
||||||
except models.User.DoesNotExist:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
if shelf_identifier:
|
|
||||||
shelf = user.shelf_set.get(identifier=shelf_identifier)
|
|
||||||
else:
|
|
||||||
shelf = user.shelf_set.first()
|
|
||||||
|
|
||||||
is_self = request.user == user
|
|
||||||
|
|
||||||
shelves = user.shelf_set
|
|
||||||
if not is_self:
|
|
||||||
follower = user.followers.filter(id=request.user.id).exists()
|
|
||||||
# make sure the user has permission to view the shelf
|
|
||||||
if shelf.privacy == 'direct' or \
|
|
||||||
(shelf.privacy == 'followers' and not follower):
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
# only show other shelves that should be visible
|
|
||||||
if follower:
|
|
||||||
shelves = shelves.filter(privacy__in=['public', 'followers'])
|
|
||||||
else:
|
|
||||||
shelves = shelves.filter(privacy='public')
|
|
||||||
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
|
||||||
|
|
||||||
books = models.ShelfBook.objects.filter(
|
|
||||||
added_by=user, shelf=shelf
|
|
||||||
).order_by('-updated_date').all()
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'title': '%s\'s %s shelf' % (user.display_name, shelf.name),
|
|
||||||
'user': user,
|
|
||||||
'is_self': is_self,
|
|
||||||
'shelves': shelves.all(),
|
|
||||||
'shelf': shelf,
|
|
||||||
'books': [b.book for b in books],
|
|
||||||
}
|
|
||||||
|
|
||||||
return TemplateResponse(request, 'shelf.html', data)
|
|
25
bookwyrm/views/__init__.py
Normal file
25
bookwyrm/views/__init__.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
''' make sure all our nice views are available '''
|
||||||
|
from .authentication import Login, Register, Logout
|
||||||
|
from .author import Author, EditAuthor
|
||||||
|
from .books import Book, EditBook, Editions
|
||||||
|
from .books import upload_cover, add_description, switch_edition, resolve_book
|
||||||
|
from .direct_message import DirectMessage
|
||||||
|
from .error import not_found_page, server_error_page
|
||||||
|
from .follow import follow, unfollow
|
||||||
|
from .follow import accept_follow_request, delete_follow_request, handle_accept
|
||||||
|
from .import_data import Import, ImportStatus
|
||||||
|
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
||||||
|
from .invite import ManageInvites, Invite
|
||||||
|
from .landing import About, Home, Feed, Discover
|
||||||
|
from .notifications import Notifications
|
||||||
|
from .outbox import Outbox
|
||||||
|
from .reading import edit_readthrough, create_readthrough, delete_readthrough
|
||||||
|
from .reading import start_reading, finish_reading
|
||||||
|
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
||||||
|
from .tag import Tag, AddTag, RemoveTag
|
||||||
|
from .search import Search
|
||||||
|
from .shelf import Shelf
|
||||||
|
from .shelf import user_shelves_page, create_shelf, delete_shelf
|
||||||
|
from .shelf import shelve, unshelve
|
||||||
|
from .status import Status, Replies, CreateStatus, DeleteStatus
|
||||||
|
from .user import User, EditUser, Followers, Following
|
113
bookwyrm/views/authentication.py
Normal file
113
bookwyrm/views/authentication.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
''' class views for login/register views '''
|
||||||
|
from django.contrib.auth import authenticate, login, logout
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Login(View):
|
||||||
|
''' authenticate an existing user '''
|
||||||
|
def get(self, request):
|
||||||
|
''' login page '''
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return redirect('/')
|
||||||
|
# sene user to the login page
|
||||||
|
data = {
|
||||||
|
'title': 'Login',
|
||||||
|
'login_form': forms.LoginForm(),
|
||||||
|
'register_form': forms.RegisterForm(),
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'login.html', data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
''' authentication action '''
|
||||||
|
login_form = forms.LoginForm(request.POST)
|
||||||
|
|
||||||
|
localname = login_form.data['localname']
|
||||||
|
username = '%s@%s' % (localname, DOMAIN)
|
||||||
|
password = login_form.data['password']
|
||||||
|
user = authenticate(request, username=username, password=password)
|
||||||
|
if user is not None:
|
||||||
|
# successful login
|
||||||
|
login(request, user)
|
||||||
|
user.last_active_date = timezone.now()
|
||||||
|
return redirect(request.GET.get('next', '/'))
|
||||||
|
|
||||||
|
# login errors
|
||||||
|
login_form.non_field_errors = 'Username or password are incorrect'
|
||||||
|
register_form = forms.RegisterForm()
|
||||||
|
data = {
|
||||||
|
'login_form': login_form,
|
||||||
|
'register_form': register_form
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'login.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
class Register(View):
|
||||||
|
''' register a user '''
|
||||||
|
def post(self, request):
|
||||||
|
''' join the server '''
|
||||||
|
if not models.SiteSettings.get().allow_registration:
|
||||||
|
invite_code = request.POST.get('invite_code')
|
||||||
|
|
||||||
|
if not invite_code:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
invite = get_object_or_404(models.SiteInvite, code=invite_code)
|
||||||
|
if not invite.valid():
|
||||||
|
raise PermissionDenied
|
||||||
|
else:
|
||||||
|
invite = None
|
||||||
|
|
||||||
|
form = forms.RegisterForm(request.POST)
|
||||||
|
errors = False
|
||||||
|
if not form.is_valid():
|
||||||
|
errors = True
|
||||||
|
|
||||||
|
localname = form.data['localname'].strip()
|
||||||
|
email = form.data['email']
|
||||||
|
password = form.data['password']
|
||||||
|
|
||||||
|
# check localname and email uniqueness
|
||||||
|
if models.User.objects.filter(localname=localname).first():
|
||||||
|
form.errors['localname'] = [
|
||||||
|
'User with this username already exists']
|
||||||
|
errors = True
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
data = {
|
||||||
|
'login_form': forms.LoginForm(),
|
||||||
|
'register_form': form,
|
||||||
|
'invite': invite,
|
||||||
|
'valid': invite.valid() if invite else True,
|
||||||
|
}
|
||||||
|
if invite:
|
||||||
|
return TemplateResponse(request, 'invite.html', data)
|
||||||
|
return TemplateResponse(request, 'login.html', data)
|
||||||
|
|
||||||
|
username = '%s@%s' % (localname, DOMAIN)
|
||||||
|
user = models.User.objects.create_user(
|
||||||
|
username, email, password, localname=localname, local=True)
|
||||||
|
if invite:
|
||||||
|
invite.times_used += 1
|
||||||
|
invite.save()
|
||||||
|
|
||||||
|
login(request, user)
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class Logout(View):
|
||||||
|
''' log out '''
|
||||||
|
def get(self, request):
|
||||||
|
''' done with this place! outa here! '''
|
||||||
|
logout(request)
|
||||||
|
return redirect('/')
|
66
bookwyrm/views/author.py
Normal file
66
bookwyrm/views/author.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
''' the good people stuff! the authors! '''
|
||||||
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from .helpers import is_api_request
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Author(View):
|
||||||
|
''' this person wrote a book '''
|
||||||
|
def get(self, request, author_id):
|
||||||
|
''' landing page for an author '''
|
||||||
|
author = get_object_or_404(models.Author, id=author_id)
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(author.to_activity())
|
||||||
|
|
||||||
|
books = models.Work.objects.filter(
|
||||||
|
Q(authors=author) | Q(editions__authors=author)).distinct()
|
||||||
|
data = {
|
||||||
|
'title': author.name,
|
||||||
|
'author': author,
|
||||||
|
'books': [b.get_default_edition() for b in books],
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'author.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
@method_decorator(
|
||||||
|
permission_required('bookwyrm.edit_book', raise_exception=True),
|
||||||
|
name='dispatch')
|
||||||
|
class EditAuthor(View):
|
||||||
|
''' edit author info '''
|
||||||
|
def get(self, request, author_id):
|
||||||
|
''' info about a book '''
|
||||||
|
author = get_object_or_404(models.Author, id=author_id)
|
||||||
|
data = {
|
||||||
|
'title': 'Edit Author',
|
||||||
|
'author': author,
|
||||||
|
'form': forms.AuthorForm(instance=author)
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'edit_author.html', data)
|
||||||
|
|
||||||
|
def post(self, request, author_id):
|
||||||
|
''' edit a author cool '''
|
||||||
|
author = get_object_or_404(models.Author, id=author_id)
|
||||||
|
|
||||||
|
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
|
||||||
|
if not form.is_valid():
|
||||||
|
data = {
|
||||||
|
'title': 'Edit Author',
|
||||||
|
'author': author,
|
||||||
|
'form': form
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'edit_author.html', data)
|
||||||
|
author = form.save()
|
||||||
|
|
||||||
|
broadcast(request.user, author.to_update_activity(request.user))
|
||||||
|
return redirect('/author/%s' % author.id)
|
228
bookwyrm/views/books.py
Normal file
228
bookwyrm/views/books.py
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
''' the good stuff! the books! '''
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Avg, Q
|
||||||
|
from django.http import HttpResponseNotFound
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from bookwyrm.connectors import connector_manager
|
||||||
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
from .helpers import is_api_request, get_activity_feed, get_edition
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Book(View):
|
||||||
|
''' a book! this is the stuff '''
|
||||||
|
def get(self, request, book_id):
|
||||||
|
''' info about a book '''
|
||||||
|
try:
|
||||||
|
page = int(request.GET.get('page', 1))
|
||||||
|
except ValueError:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||||
|
except models.Book.DoesNotExist:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(book.to_activity())
|
||||||
|
|
||||||
|
if isinstance(book, models.Work):
|
||||||
|
book = book.get_default_edition()
|
||||||
|
if not book:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
work = book.parent_work
|
||||||
|
if not work:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
reviews = models.Review.objects.filter(
|
||||||
|
book__in=work.editions.all(),
|
||||||
|
)
|
||||||
|
# all reviews for the book
|
||||||
|
reviews = get_activity_feed(
|
||||||
|
request.user,
|
||||||
|
['public', 'unlisted', 'followers', 'direct'],
|
||||||
|
queryset=reviews
|
||||||
|
)
|
||||||
|
|
||||||
|
# the reviews to show
|
||||||
|
paginated = Paginator(reviews.exclude(
|
||||||
|
Q(content__isnull=True) | Q(content='')
|
||||||
|
), PAGE_LENGTH)
|
||||||
|
reviews_page = paginated.page(page)
|
||||||
|
|
||||||
|
user_tags = readthroughs = user_shelves = other_edition_shelves = []
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
user_tags = models.UserTag.objects.filter(
|
||||||
|
book=book, user=request.user
|
||||||
|
).values_list('tag__identifier', flat=True)
|
||||||
|
|
||||||
|
readthroughs = models.ReadThrough.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
book=book,
|
||||||
|
).order_by('start_date')
|
||||||
|
|
||||||
|
user_shelves = models.ShelfBook.objects.filter(
|
||||||
|
added_by=request.user, book=book
|
||||||
|
)
|
||||||
|
|
||||||
|
other_edition_shelves = models.ShelfBook.objects.filter(
|
||||||
|
~Q(book=book),
|
||||||
|
added_by=request.user,
|
||||||
|
book__parent_work=book.parent_work,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': book.title,
|
||||||
|
'book': book,
|
||||||
|
'reviews': reviews_page,
|
||||||
|
'review_count': reviews.count(),
|
||||||
|
'ratings': reviews.filter(Q(content__isnull=True) | Q(content='')),
|
||||||
|
'rating': reviews.aggregate(Avg('rating'))['rating__avg'],
|
||||||
|
'tags': models.UserTag.objects.filter(book=book),
|
||||||
|
'user_tags': user_tags,
|
||||||
|
'user_shelves': user_shelves,
|
||||||
|
'other_edition_shelves': other_edition_shelves,
|
||||||
|
'readthroughs': readthroughs,
|
||||||
|
'path': '/book/%s' % book_id,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'book.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
@method_decorator(
|
||||||
|
permission_required('bookwyrm.edit_book', raise_exception=True),
|
||||||
|
name='dispatch')
|
||||||
|
class EditBook(View):
|
||||||
|
''' edit a book '''
|
||||||
|
def get(self, request, book_id):
|
||||||
|
''' info about a book '''
|
||||||
|
book = get_edition(book_id)
|
||||||
|
if not book.description:
|
||||||
|
book.description = book.parent_work.description
|
||||||
|
data = {
|
||||||
|
'title': 'Edit Book',
|
||||||
|
'book': book,
|
||||||
|
'form': forms.EditionForm(instance=book)
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'edit_book.html', data)
|
||||||
|
|
||||||
|
def post(self, request, book_id):
|
||||||
|
''' edit a book cool '''
|
||||||
|
book = get_object_or_404(models.Edition, id=book_id)
|
||||||
|
|
||||||
|
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||||
|
if not form.is_valid():
|
||||||
|
data = {
|
||||||
|
'title': 'Edit Book',
|
||||||
|
'book': book,
|
||||||
|
'form': form
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'edit_book.html', data)
|
||||||
|
book = form.save()
|
||||||
|
|
||||||
|
broadcast(request.user, book.to_update_activity(request.user))
|
||||||
|
return redirect('/book/%s' % book.id)
|
||||||
|
|
||||||
|
|
||||||
|
class Editions(View):
|
||||||
|
''' list of editions '''
|
||||||
|
def get(self, request, book_id):
|
||||||
|
''' list of editions of a book '''
|
||||||
|
work = get_object_or_404(models.Work, id=book_id)
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(work.to_edition_list(**request.GET))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': 'Editions of %s' % work.title,
|
||||||
|
'editions': work.editions.order_by('-edition_rank').all(),
|
||||||
|
'work': work,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'editions.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def upload_cover(request, book_id):
|
||||||
|
''' upload a new cover '''
|
||||||
|
book = get_object_or_404(models.Edition, id=book_id)
|
||||||
|
|
||||||
|
form = forms.CoverForm(request.POST, request.FILES, instance=book)
|
||||||
|
if not form.is_valid():
|
||||||
|
return redirect('/book/%d' % book.id)
|
||||||
|
|
||||||
|
book.cover = form.files['cover']
|
||||||
|
book.save()
|
||||||
|
|
||||||
|
broadcast(request.user, book.to_update_activity(request.user))
|
||||||
|
return redirect('/book/%s' % book.id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
@permission_required('bookwyrm.edit_book', raise_exception=True)
|
||||||
|
def add_description(request, book_id):
|
||||||
|
''' upload a new cover '''
|
||||||
|
if not request.method == 'POST':
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
book = get_object_or_404(models.Edition, id=book_id)
|
||||||
|
|
||||||
|
description = request.POST.get('description')
|
||||||
|
|
||||||
|
book.description = description
|
||||||
|
book.save()
|
||||||
|
|
||||||
|
broadcast(request.user, book.to_update_activity(request.user))
|
||||||
|
return redirect('/book/%s' % book.id)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def resolve_book(request):
|
||||||
|
''' figure out the local path to a book from a remote_id '''
|
||||||
|
remote_id = request.POST.get('remote_id')
|
||||||
|
connector = connector_manager.get_or_create_connector(remote_id)
|
||||||
|
book = connector.get_or_create_book(remote_id)
|
||||||
|
|
||||||
|
return redirect('/book/%d' % book.id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
@transaction.atomic
|
||||||
|
def switch_edition(request):
|
||||||
|
''' switch your copy of a book to a different edition '''
|
||||||
|
edition_id = request.POST.get('edition')
|
||||||
|
new_edition = get_object_or_404(models.Edition, id=edition_id)
|
||||||
|
shelfbooks = models.ShelfBook.objects.filter(
|
||||||
|
book__parent_work=new_edition.parent_work,
|
||||||
|
shelf__user=request.user
|
||||||
|
)
|
||||||
|
for shelfbook in shelfbooks.all():
|
||||||
|
broadcast(request.user, shelfbook.to_remove_activity(request.user))
|
||||||
|
|
||||||
|
shelfbook.book = new_edition
|
||||||
|
shelfbook.save()
|
||||||
|
|
||||||
|
broadcast(request.user, shelfbook.to_add_activity(request.user))
|
||||||
|
|
||||||
|
readthroughs = models.ReadThrough.objects.filter(
|
||||||
|
book__parent_work=new_edition.parent_work,
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
for readthrough in readthroughs.all():
|
||||||
|
readthrough.book = new_edition
|
||||||
|
readthrough.save()
|
||||||
|
|
||||||
|
return redirect('/book/%d' % new_edition.id)
|
26
bookwyrm/views/direct_message.py
Normal file
26
bookwyrm/views/direct_message.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
''' non-interactive pages '''
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
from .helpers import get_activity_feed
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class DirectMessage(View):
|
||||||
|
''' dm view '''
|
||||||
|
def get(self, request, page=1):
|
||||||
|
''' like a feed but for dms only '''
|
||||||
|
activities = get_activity_feed(request.user, 'direct')
|
||||||
|
paginated = Paginator(activities, PAGE_LENGTH)
|
||||||
|
activity_page = paginated.page(page)
|
||||||
|
data = {
|
||||||
|
'title': 'Direct Messages',
|
||||||
|
'user': request.user,
|
||||||
|
'activities': activity_page,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'direct_messages.html', data)
|
13
bookwyrm/views/error.py
Normal file
13
bookwyrm/views/error.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
''' something has gone amiss '''
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
|
||||||
|
def server_error_page(request):
|
||||||
|
''' 500 errors '''
|
||||||
|
return TemplateResponse(
|
||||||
|
request, 'error.html', {'title': 'Oops!'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
def not_found_page(request, _):
|
||||||
|
''' 404s '''
|
||||||
|
return TemplateResponse(
|
||||||
|
request, 'notfound.html', {'title': 'Not found'}, status=404)
|
113
bookwyrm/views/follow.py
Normal file
113
bookwyrm/views/follow.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
''' views for actions you can take in the application '''
|
||||||
|
from django.db import transaction
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from .helpers import get_user_from_username
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def follow(request):
|
||||||
|
''' follow another user, here or abroad '''
|
||||||
|
username = request.POST['user']
|
||||||
|
try:
|
||||||
|
to_follow = get_user_from_username(username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
relationship, _ = models.UserFollowRequest.objects.get_or_create(
|
||||||
|
user_subject=request.user,
|
||||||
|
user_object=to_follow,
|
||||||
|
)
|
||||||
|
activity = relationship.to_activity()
|
||||||
|
broadcast(
|
||||||
|
request.user, activity, privacy='direct', direct_recipients=[to_follow])
|
||||||
|
return redirect(to_follow.local_path)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def unfollow(request):
|
||||||
|
''' unfollow a user '''
|
||||||
|
username = request.POST['user']
|
||||||
|
try:
|
||||||
|
to_unfollow = get_user_from_username(username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
relationship = models.UserFollows.objects.get(
|
||||||
|
user_subject=request.user,
|
||||||
|
user_object=to_unfollow
|
||||||
|
)
|
||||||
|
activity = relationship.to_undo_activity(request.user)
|
||||||
|
broadcast(
|
||||||
|
request.user, activity,
|
||||||
|
privacy='direct', direct_recipients=[to_unfollow])
|
||||||
|
|
||||||
|
to_unfollow.followers.remove(request.user)
|
||||||
|
return redirect(to_unfollow.local_path)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def accept_follow_request(request):
|
||||||
|
''' a user accepts a follow request '''
|
||||||
|
username = request.POST['user']
|
||||||
|
try:
|
||||||
|
requester = get_user_from_username(username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
try:
|
||||||
|
follow_request = models.UserFollowRequest.objects.get(
|
||||||
|
user_subject=requester,
|
||||||
|
user_object=request.user
|
||||||
|
)
|
||||||
|
except models.UserFollowRequest.DoesNotExist:
|
||||||
|
# Request already dealt with.
|
||||||
|
return redirect(request.user.local_path)
|
||||||
|
handle_accept(follow_request)
|
||||||
|
|
||||||
|
return redirect(request.user.local_path)
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
relationship.save()
|
||||||
|
|
||||||
|
activity = relationship.to_accept_activity()
|
||||||
|
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def delete_follow_request(request):
|
||||||
|
''' a user rejects a follow request '''
|
||||||
|
username = request.POST['user']
|
||||||
|
try:
|
||||||
|
requester = get_user_from_username(username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
try:
|
||||||
|
follow_request = models.UserFollowRequest.objects.get(
|
||||||
|
user_subject=requester,
|
||||||
|
user_object=request.user
|
||||||
|
)
|
||||||
|
except models.UserFollowRequest.DoesNotExist:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
activity = follow_request.to_reject_activity()
|
||||||
|
follow_request.delete()
|
||||||
|
broadcast(
|
||||||
|
request.user, activity, privacy='direct', direct_recipients=[requester])
|
||||||
|
return redirect('/user/%s' % request.user.localname)
|
173
bookwyrm/views/helpers.py
Normal file
173
bookwyrm/views/helpers.py
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
''' helper functions used in various views '''
|
||||||
|
import re
|
||||||
|
from requests import HTTPError
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from bookwyrm import activitypub, models
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from bookwyrm.connectors import ConnectorException, get_data
|
||||||
|
from bookwyrm.status import create_generated_note
|
||||||
|
from bookwyrm.utils import regex
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_from_username(username):
|
||||||
|
''' helper function to resolve a localname or a username to a user '''
|
||||||
|
# raises DoesNotExist if user is now found
|
||||||
|
try:
|
||||||
|
return models.User.objects.get(localname=username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return models.User.objects.get(username=username)
|
||||||
|
|
||||||
|
|
||||||
|
def is_api_request(request):
|
||||||
|
''' check whether a request is asking for html or data '''
|
||||||
|
return 'json' in request.headers.get('Accept') or \
|
||||||
|
request.path[-5:] == '.json'
|
||||||
|
|
||||||
|
|
||||||
|
def is_bookworm_request(request):
|
||||||
|
''' check if the request is coming from another bookworm instance '''
|
||||||
|
user_agent = request.headers.get('User-Agent')
|
||||||
|
if user_agent is None or \
|
||||||
|
re.search(regex.bookwyrm_user_agent, user_agent) is None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def status_visible_to_user(viewer, status):
|
||||||
|
''' is a user authorized to view a status? '''
|
||||||
|
if viewer == status.user or status.privacy in ['public', 'unlisted']:
|
||||||
|
return True
|
||||||
|
if status.privacy == 'followers' and \
|
||||||
|
status.user.followers.filter(id=viewer.id).first():
|
||||||
|
return True
|
||||||
|
if status.privacy == 'direct' and \
|
||||||
|
status.mention_users.filter(id=viewer.id).first():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_activity_feed(
|
||||||
|
user, privacy, local_only=False, following_only=False,
|
||||||
|
queryset=models.Status.objects):
|
||||||
|
''' get a filtered queryset of statuses '''
|
||||||
|
privacy = privacy if isinstance(privacy, list) else [privacy]
|
||||||
|
# if we're looking at Status, we need this. We don't if it's Comment
|
||||||
|
if hasattr(queryset, 'select_subclasses'):
|
||||||
|
queryset = queryset.select_subclasses()
|
||||||
|
|
||||||
|
# exclude deleted
|
||||||
|
queryset = queryset.exclude(deleted=True).order_by('-published_date')
|
||||||
|
|
||||||
|
# you can't see followers only or direct messages if you're not logged in
|
||||||
|
if user.is_anonymous:
|
||||||
|
privacy = [p for p in privacy if not p in ['followers', 'direct']]
|
||||||
|
|
||||||
|
# filter to only privided privacy levels
|
||||||
|
queryset = queryset.filter(privacy__in=privacy)
|
||||||
|
|
||||||
|
# only include statuses the user follows
|
||||||
|
if following_only:
|
||||||
|
queryset = queryset.exclude(
|
||||||
|
~Q(# remove everythign except
|
||||||
|
Q(user__in=user.following.all()) | # user follwoing
|
||||||
|
Q(user=user) |# is self
|
||||||
|
Q(mention_users=user)# mentions user
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# exclude followers-only statuses the user doesn't follow
|
||||||
|
elif 'followers' in privacy:
|
||||||
|
queryset = queryset.exclude(
|
||||||
|
~Q(# user isn't following and it isn't their own status
|
||||||
|
Q(user__in=user.following.all()) | Q(user=user)
|
||||||
|
),
|
||||||
|
privacy='followers' # and the status is followers only
|
||||||
|
)
|
||||||
|
|
||||||
|
# exclude direct messages not intended for the user
|
||||||
|
if 'direct' in privacy:
|
||||||
|
queryset = queryset.exclude(
|
||||||
|
~Q(
|
||||||
|
Q(user=user) | Q(mention_users=user)
|
||||||
|
), privacy='direct'
|
||||||
|
)
|
||||||
|
|
||||||
|
# filter for only local status
|
||||||
|
if local_only:
|
||||||
|
queryset = queryset.filter(user__local=True)
|
||||||
|
|
||||||
|
# remove statuses that have boosts in the same queryset
|
||||||
|
try:
|
||||||
|
queryset = queryset.filter(~Q(boosters__in=queryset))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def handle_remote_webfinger(query):
|
||||||
|
''' webfingerin' other servers '''
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# usernames could be @user@domain or user@domain
|
||||||
|
if not query:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if query[0] == '@':
|
||||||
|
query = query[1:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
domain = query.split('@')[1]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = models.User.objects.get(username=query)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \
|
||||||
|
(domain, query)
|
||||||
|
try:
|
||||||
|
data = get_data(url)
|
||||||
|
except (ConnectorException, HTTPError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
for link in data.get('links'):
|
||||||
|
if link.get('rel') == 'self':
|
||||||
|
try:
|
||||||
|
user = activitypub.resolve_remote_id(
|
||||||
|
models.User, link['href']
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def get_edition(book_id):
|
||||||
|
''' look up a book in the db and return an edition '''
|
||||||
|
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||||
|
if isinstance(book, models.Work):
|
||||||
|
book = book.get_default_edition()
|
||||||
|
return book
|
||||||
|
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
'to-read': 'wants to read',
|
||||||
|
'reading': 'started reading',
|
||||||
|
'read': 'finished reading'
|
||||||
|
}[shelf.identifier]
|
||||||
|
except KeyError:
|
||||||
|
# it's a non-standard shelf, don't worry about it
|
||||||
|
return
|
||||||
|
|
||||||
|
status = create_generated_note(
|
||||||
|
user,
|
||||||
|
message,
|
||||||
|
mention_books=[book],
|
||||||
|
privacy=privacy
|
||||||
|
)
|
||||||
|
status.save()
|
||||||
|
|
||||||
|
broadcast(user, status.to_create_activity(user))
|
83
bookwyrm/views/import_data.py
Normal file
83
bookwyrm/views/import_data.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
''' import books from another app '''
|
||||||
|
from io import TextIOWrapper
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import forms, goodreads_import, models
|
||||||
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class Import(View):
|
||||||
|
''' import view '''
|
||||||
|
def get(self, request):
|
||||||
|
''' load import page '''
|
||||||
|
return TemplateResponse(request, 'import.html', {
|
||||||
|
'title': 'Import Books',
|
||||||
|
'import_form': forms.ImportForm(),
|
||||||
|
'jobs': models.ImportJob.
|
||||||
|
objects.filter(user=request.user).order_by('-created_date'),
|
||||||
|
})
|
||||||
|
|
||||||
|
def post(self, 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),
|
||||||
|
include_reviews,
|
||||||
|
privacy,
|
||||||
|
)
|
||||||
|
except (UnicodeDecodeError, ValueError):
|
||||||
|
return HttpResponseBadRequest('Not a valid csv file')
|
||||||
|
goodreads_import.start_import(job)
|
||||||
|
return redirect('/import-status/%d' % job.id)
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class ImportStatus(View):
|
||||||
|
''' status of an existing import '''
|
||||||
|
def get(self, request, job_id):
|
||||||
|
''' status of an import job '''
|
||||||
|
job = models.ImportJob.objects.get(id=job_id)
|
||||||
|
if job.user != request.user:
|
||||||
|
raise PermissionDenied
|
||||||
|
task = app.AsyncResult(job.task_id)
|
||||||
|
items = job.items.order_by('index').all()
|
||||||
|
failed_items = [i for i in items if i.fail_reason]
|
||||||
|
items = [i for i in items if not i.fail_reason]
|
||||||
|
return TemplateResponse(request, 'import_status.html', {
|
||||||
|
'title': 'Import Status',
|
||||||
|
'job': job,
|
||||||
|
'items': items,
|
||||||
|
'failed_items': failed_items,
|
||||||
|
'task': task
|
||||||
|
})
|
||||||
|
|
||||||
|
def post(self, request, job_id):
|
||||||
|
''' retry lines from an import '''
|
||||||
|
job = get_object_or_404(models.ImportJob, id=job_id)
|
||||||
|
items = []
|
||||||
|
for item in request.POST.getlist('import_item'):
|
||||||
|
items.append(get_object_or_404(models.ImportItem, id=item))
|
||||||
|
|
||||||
|
job = goodreads_import.create_retry_job(
|
||||||
|
request.user,
|
||||||
|
job,
|
||||||
|
items,
|
||||||
|
)
|
||||||
|
goodreads_import.start_import(job)
|
||||||
|
return redirect('/import-status/%d' % job.id)
|
130
bookwyrm/views/interaction.py
Normal file
130
bookwyrm/views/interaction.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
''' boosts and favs '''
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from bookwyrm.status import create_notification
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class Favorite(View):
|
||||||
|
''' like a status '''
|
||||||
|
def post(self, request, status_id):
|
||||||
|
''' create a like '''
|
||||||
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
try:
|
||||||
|
favorite = models.Favorite.objects.create(
|
||||||
|
status=status,
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
except IntegrityError:
|
||||||
|
# you already fav'ed that
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
fav_activity = favorite.to_activity()
|
||||||
|
broadcast(
|
||||||
|
request.user, fav_activity, privacy='direct',
|
||||||
|
direct_recipients=[status.user])
|
||||||
|
if status.user.local:
|
||||||
|
create_notification(
|
||||||
|
status.user,
|
||||||
|
'FAVORITE',
|
||||||
|
related_user=request.user,
|
||||||
|
related_status=status
|
||||||
|
)
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class Unfavorite(View):
|
||||||
|
''' take back a fav '''
|
||||||
|
def post(self, request, status_id):
|
||||||
|
''' unlike a status '''
|
||||||
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
try:
|
||||||
|
favorite = models.Favorite.objects.get(
|
||||||
|
status=status,
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
except models.Favorite.DoesNotExist:
|
||||||
|
# can't find that status, idk
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
fav_activity = favorite.to_undo_activity(request.user)
|
||||||
|
favorite.delete()
|
||||||
|
broadcast(request.user, fav_activity, direct_recipients=[status.user])
|
||||||
|
|
||||||
|
# check for notification
|
||||||
|
if status.user.local:
|
||||||
|
notification = models.Notification.objects.filter(
|
||||||
|
user=status.user, related_user=request.user,
|
||||||
|
related_status=status, notification_type='FAVORITE'
|
||||||
|
).first()
|
||||||
|
if notification:
|
||||||
|
notification.delete()
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class Boost(View):
|
||||||
|
''' boost a status '''
|
||||||
|
def post(self, request, status_id):
|
||||||
|
''' boost a status '''
|
||||||
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
# is it boostable?
|
||||||
|
if not status.boostable:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
if models.Boost.objects.filter(
|
||||||
|
boosted_status=status, user=request.user).exists():
|
||||||
|
# you already boosted that.
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
boost = models.Boost.objects.create(
|
||||||
|
boosted_status=status,
|
||||||
|
privacy=status.privacy,
|
||||||
|
user=request.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
boost_activity = boost.to_activity()
|
||||||
|
broadcast(request.user, boost_activity)
|
||||||
|
|
||||||
|
if status.user.local:
|
||||||
|
create_notification(
|
||||||
|
status.user,
|
||||||
|
'BOOST',
|
||||||
|
related_user=request.user,
|
||||||
|
related_status=status
|
||||||
|
)
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class Unboost(View):
|
||||||
|
''' boost a status '''
|
||||||
|
def post(self, request, status_id):
|
||||||
|
''' boost a status '''
|
||||||
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
boost = models.Boost.objects.filter(
|
||||||
|
boosted_status=status, user=request.user
|
||||||
|
).first()
|
||||||
|
activity = boost.to_undo_activity(request.user)
|
||||||
|
|
||||||
|
boost.delete()
|
||||||
|
broadcast(request.user, activity)
|
||||||
|
|
||||||
|
# delete related notification
|
||||||
|
if status.user.local:
|
||||||
|
notification = models.Notification.objects.filter(
|
||||||
|
user=status.user, related_user=request.user,
|
||||||
|
related_status=status, notification_type='BOOST'
|
||||||
|
).first()
|
||||||
|
if notification:
|
||||||
|
notification.delete()
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
58
bookwyrm/views/invite.py
Normal file
58
bookwyrm/views/invite.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
''' invites when registration is closed '''
|
||||||
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
@method_decorator(
|
||||||
|
permission_required('bookwyrm.create_invites', raise_exception=True),
|
||||||
|
name='dispatch')
|
||||||
|
class ManageInvites(View):
|
||||||
|
''' create invites '''
|
||||||
|
def get(self, request):
|
||||||
|
''' invite management page '''
|
||||||
|
data = {
|
||||||
|
'title': 'Invitations',
|
||||||
|
'invites': models.SiteInvite.objects.filter(
|
||||||
|
user=request.user).order_by('-created_date'),
|
||||||
|
'form': forms.CreateInviteForm(),
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'manage_invites.html', data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
''' creates an invite database entry '''
|
||||||
|
form = forms.CreateInviteForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
|
||||||
|
|
||||||
|
invite = form.save(commit=False)
|
||||||
|
invite.user = request.user
|
||||||
|
invite.save()
|
||||||
|
|
||||||
|
return redirect('/invite')
|
||||||
|
|
||||||
|
|
||||||
|
class Invite(View):
|
||||||
|
''' use an invite to register '''
|
||||||
|
def get(self, request, code):
|
||||||
|
''' endpoint for using an invites '''
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return redirect('/')
|
||||||
|
invite = get_object_or_404(models.SiteInvite, code=code)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': 'Join',
|
||||||
|
'register_form': forms.RegisterForm(),
|
||||||
|
'invite': invite,
|
||||||
|
'valid': invite.valid() if invite else True,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'invite.html', data)
|
||||||
|
|
||||||
|
# post handling is in views.authentication.Register
|
122
bookwyrm/views/landing.py
Normal file
122
bookwyrm/views/landing.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
''' non-interactive pages '''
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Avg, Max
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
from .helpers import get_activity_feed
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class About(View):
|
||||||
|
''' create invites '''
|
||||||
|
def get(self, request):
|
||||||
|
''' more information about the instance '''
|
||||||
|
data = {
|
||||||
|
'title': 'About',
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'about.html', data)
|
||||||
|
|
||||||
|
class Home(View):
|
||||||
|
''' discover page or home feed depending on auth '''
|
||||||
|
def get(self, request):
|
||||||
|
''' this is the same as the feed on the home tab '''
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
feed_view = Feed.as_view()
|
||||||
|
return feed_view(request, 'home')
|
||||||
|
discover_view = Discover.as_view()
|
||||||
|
return discover_view(request)
|
||||||
|
|
||||||
|
class Discover(View):
|
||||||
|
''' preview of recently reviewed books '''
|
||||||
|
def get(self, request):
|
||||||
|
''' tiled book activity page '''
|
||||||
|
books = models.Edition.objects.filter(
|
||||||
|
review__published_date__isnull=False,
|
||||||
|
review__user__local=True,
|
||||||
|
review__privacy__in=['public', 'unlisted'],
|
||||||
|
).exclude(
|
||||||
|
cover__exact=''
|
||||||
|
).annotate(
|
||||||
|
Max('review__published_date')
|
||||||
|
).order_by('-review__published_date__max')[:6]
|
||||||
|
|
||||||
|
ratings = {}
|
||||||
|
for book in books:
|
||||||
|
reviews = models.Review.objects.filter(
|
||||||
|
book__in=book.parent_work.editions.all()
|
||||||
|
)
|
||||||
|
reviews = get_activity_feed(
|
||||||
|
request.user, ['public', 'unlisted'], queryset=reviews)
|
||||||
|
ratings[book.id] = reviews.aggregate(Avg('rating'))['rating__avg']
|
||||||
|
data = {
|
||||||
|
'title': 'Discover',
|
||||||
|
'register_form': forms.RegisterForm(),
|
||||||
|
'books': list(set(books)),
|
||||||
|
'ratings': ratings
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'discover.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class Feed(View):
|
||||||
|
''' activity stream '''
|
||||||
|
def get(self, request, tab):
|
||||||
|
''' user's homepage with activity feed '''
|
||||||
|
try:
|
||||||
|
page = int(request.GET.get('page', 1))
|
||||||
|
except ValueError:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
suggested_books = get_suggested_books(request.user)
|
||||||
|
|
||||||
|
if tab == 'home':
|
||||||
|
activities = get_activity_feed(
|
||||||
|
request.user, ['public', 'unlisted', 'followers'],
|
||||||
|
following_only=True)
|
||||||
|
elif tab == 'local':
|
||||||
|
activities = get_activity_feed(
|
||||||
|
request.user, ['public', 'followers'], local_only=True)
|
||||||
|
else:
|
||||||
|
activities = get_activity_feed(
|
||||||
|
request.user, ['public', 'followers'])
|
||||||
|
paginated = Paginator(activities, PAGE_LENGTH)
|
||||||
|
data = {
|
||||||
|
'title': 'Updates Feed',
|
||||||
|
'user': request.user,
|
||||||
|
'suggested_books': suggested_books,
|
||||||
|
'activities': paginated.page(page),
|
||||||
|
'tab': tab,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'feed.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_suggested_books(user, max_books=5):
|
||||||
|
''' helper to get a user's recent books '''
|
||||||
|
book_count = 0
|
||||||
|
preset_shelves = [
|
||||||
|
('reading', max_books), ('read', 2), ('to-read', max_books)
|
||||||
|
]
|
||||||
|
suggested_books = []
|
||||||
|
for (preset, shelf_max) in preset_shelves:
|
||||||
|
limit = shelf_max if shelf_max < (max_books - book_count) \
|
||||||
|
else max_books - book_count
|
||||||
|
shelf = user.shelf_set.get(identifier=preset)
|
||||||
|
|
||||||
|
shelf_books = shelf.shelfbook_set.order_by(
|
||||||
|
'-updated_date'
|
||||||
|
).all()[:limit]
|
||||||
|
if not shelf_books:
|
||||||
|
continue
|
||||||
|
shelf_preview = {
|
||||||
|
'name': shelf.name,
|
||||||
|
'books': [s.book for s in shelf_books]
|
||||||
|
}
|
||||||
|
suggested_books.append(shelf_preview)
|
||||||
|
book_count += len(shelf_preview['books'])
|
||||||
|
return suggested_books
|
29
bookwyrm/views/notifications.py
Normal file
29
bookwyrm/views/notifications.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
''' non-interactive pages '''
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class Notifications(View):
|
||||||
|
''' notifications view '''
|
||||||
|
def get(self, request):
|
||||||
|
''' people are interacting with you, get hyped '''
|
||||||
|
notifications = request.user.notification_set.all() \
|
||||||
|
.order_by('-created_date')
|
||||||
|
unread = [n.id for n in notifications.filter(read=False)]
|
||||||
|
data = {
|
||||||
|
'title': 'Notifications',
|
||||||
|
'notifications': notifications,
|
||||||
|
'unread': unread,
|
||||||
|
}
|
||||||
|
notifications.update(read=True)
|
||||||
|
return TemplateResponse(request, 'notifications.html', data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
''' permanently delete notification for user '''
|
||||||
|
request.user.notification_set.filter(read=True).delete()
|
||||||
|
return redirect('/notifications')
|
22
bookwyrm/views/outbox.py
Normal file
22
bookwyrm/views/outbox.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
''' the good stuff! the books! '''
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import activitypub, models
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Outbox(View):
|
||||||
|
''' outbox '''
|
||||||
|
def get(self, request, username):
|
||||||
|
''' outbox for the requested user '''
|
||||||
|
user = get_object_or_404(models.User, localname=username)
|
||||||
|
filter_type = request.GET.get('type')
|
||||||
|
if filter_type not in models.status_models:
|
||||||
|
filter_type = None
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
user.to_outbox(**request.GET, filter_type=filter_type),
|
||||||
|
encoder=activitypub.ActivityEncoder
|
||||||
|
)
|
102
bookwyrm/views/password.py
Normal file
102
bookwyrm/views/password.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
''' class views for password management '''
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.emailing import password_reset_email
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class PasswordResetRequest(View):
|
||||||
|
''' forgot password flow '''
|
||||||
|
def get(self, request):
|
||||||
|
''' password reset page '''
|
||||||
|
return TemplateResponse(
|
||||||
|
request,
|
||||||
|
'password_reset_request.html',
|
||||||
|
{'title': 'Reset Password'}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
''' create a password reset token '''
|
||||||
|
email = request.POST.get('email')
|
||||||
|
try:
|
||||||
|
user = models.User.objects.get(email=email)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return redirect('/password-reset')
|
||||||
|
|
||||||
|
# remove any existing password reset cods for this user
|
||||||
|
models.PasswordReset.objects.filter(user=user).all().delete()
|
||||||
|
|
||||||
|
# create a new reset code
|
||||||
|
code = models.PasswordReset.objects.create(user=user)
|
||||||
|
password_reset_email(code)
|
||||||
|
data = {'message': 'Password reset link sent to %s' % email}
|
||||||
|
return TemplateResponse(request, 'password_reset_request.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordReset(View):
|
||||||
|
''' set new password '''
|
||||||
|
def get(self, request, code):
|
||||||
|
''' endpoint for sending invites '''
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return redirect('/')
|
||||||
|
try:
|
||||||
|
reset_code = models.PasswordReset.objects.get(code=code)
|
||||||
|
if not reset_code.valid():
|
||||||
|
raise PermissionDenied
|
||||||
|
except models.PasswordReset.DoesNotExist:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
return TemplateResponse(
|
||||||
|
request,
|
||||||
|
'password_reset.html',
|
||||||
|
{'title': 'Reset Password', 'code': reset_code.code}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, request, code):
|
||||||
|
''' allow a user to change their password through an emailed token '''
|
||||||
|
try:
|
||||||
|
reset_code = models.PasswordReset.objects.get(
|
||||||
|
code=code
|
||||||
|
)
|
||||||
|
except models.PasswordReset.DoesNotExist:
|
||||||
|
data = {'errors': ['Invalid password reset link']}
|
||||||
|
return TemplateResponse(request, 'password_reset.html', data)
|
||||||
|
|
||||||
|
user = reset_code.user
|
||||||
|
|
||||||
|
new_password = request.POST.get('password')
|
||||||
|
confirm_password = request.POST.get('confirm-password')
|
||||||
|
|
||||||
|
if new_password != confirm_password:
|
||||||
|
data = {'errors': ['Passwords do not match']}
|
||||||
|
return TemplateResponse(request, 'password_reset.html', data)
|
||||||
|
|
||||||
|
user.set_password(new_password)
|
||||||
|
user.save()
|
||||||
|
login(request, user)
|
||||||
|
reset_code.delete()
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class ChangePassword(View):
|
||||||
|
''' change password as logged in user '''
|
||||||
|
def post(self, request):
|
||||||
|
''' allow a user to change their password '''
|
||||||
|
new_password = request.POST.get('password')
|
||||||
|
confirm_password = request.POST.get('confirm-password')
|
||||||
|
|
||||||
|
if new_password != confirm_password:
|
||||||
|
return redirect('/edit-profile')
|
||||||
|
|
||||||
|
request.user.set_password(new_password)
|
||||||
|
request.user.save()
|
||||||
|
login(request, request.user)
|
||||||
|
return redirect('/user/%s' % request.user.localname)
|
172
bookwyrm/views/reading.py
Normal file
172
bookwyrm/views/reading.py
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
''' the good stuff! the books! '''
|
||||||
|
import dateutil.parser
|
||||||
|
from dateutil.parser import ParserError
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from .helpers import get_edition, handle_reading_status
|
||||||
|
from .shelf import handle_unshelve
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def start_reading(request, book_id):
|
||||||
|
''' begin reading a book '''
|
||||||
|
book = get_edition(book_id)
|
||||||
|
shelf = models.Shelf.objects.filter(
|
||||||
|
identifier='reading',
|
||||||
|
user=request.user
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# create a readthrough
|
||||||
|
readthrough = update_readthrough(request, book=book)
|
||||||
|
if readthrough:
|
||||||
|
readthrough.save()
|
||||||
|
|
||||||
|
# shelve the book
|
||||||
|
if request.POST.get('reshelve', True):
|
||||||
|
try:
|
||||||
|
current_shelf = models.Shelf.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
edition=book
|
||||||
|
)
|
||||||
|
handle_unshelve(request.user, book, current_shelf)
|
||||||
|
except models.Shelf.DoesNotExist:
|
||||||
|
# this just means it isn't currently on the user's shelves
|
||||||
|
pass
|
||||||
|
shelfbook = models.ShelfBook.objects.create(
|
||||||
|
book=book, shelf=shelf, added_by=request.user)
|
||||||
|
broadcast(request.user, shelfbook.to_add_activity(request.user))
|
||||||
|
|
||||||
|
# post about it (if you want)
|
||||||
|
if request.POST.get('post-status'):
|
||||||
|
privacy = request.POST.get('privacy')
|
||||||
|
handle_reading_status(request.user, shelf, book, privacy)
|
||||||
|
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def finish_reading(request, book_id):
|
||||||
|
''' a user completed a book, yay '''
|
||||||
|
book = get_edition(book_id)
|
||||||
|
shelf = models.Shelf.objects.filter(
|
||||||
|
identifier='read',
|
||||||
|
user=request.user
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# update or create a readthrough
|
||||||
|
readthrough = update_readthrough(request, book=book)
|
||||||
|
if readthrough:
|
||||||
|
readthrough.save()
|
||||||
|
|
||||||
|
# shelve the book
|
||||||
|
if request.POST.get('reshelve', True):
|
||||||
|
try:
|
||||||
|
current_shelf = models.Shelf.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
edition=book
|
||||||
|
)
|
||||||
|
handle_unshelve(request.user, book, current_shelf)
|
||||||
|
except models.Shelf.DoesNotExist:
|
||||||
|
# this just means it isn't currently on the user's shelves
|
||||||
|
pass
|
||||||
|
shelfbook = models.ShelfBook.objects.create(
|
||||||
|
book=book, shelf=shelf, added_by=request.user)
|
||||||
|
broadcast(request.user, shelfbook.to_add_activity(request.user))
|
||||||
|
|
||||||
|
# post about it (if you want)
|
||||||
|
if request.POST.get('post-status'):
|
||||||
|
privacy = request.POST.get('privacy')
|
||||||
|
handle_reading_status(request.user, shelf, book, privacy)
|
||||||
|
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
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
|
||||||
|
@require_POST
|
||||||
|
def delete_readthrough(request):
|
||||||
|
''' remove a readthrough '''
|
||||||
|
readthrough = get_object_or_404(
|
||||||
|
models.ReadThrough, id=request.POST.get('id'))
|
||||||
|
|
||||||
|
# don't let people edit other people's data
|
||||||
|
if request.user != readthrough.user:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
readthrough.delete()
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def create_readthrough(request):
|
||||||
|
''' can't use the form because the dates are too finnicky '''
|
||||||
|
book = get_object_or_404(models.Edition, id=request.POST.get('book'))
|
||||||
|
readthrough = update_readthrough(request, create=True, book=book)
|
||||||
|
if not readthrough:
|
||||||
|
return redirect(book.local_path)
|
||||||
|
readthrough.save()
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
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 = timezone.make_aware(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 = timezone.make_aware(
|
||||||
|
dateutil.parser.parse(finish_date))
|
||||||
|
readthrough.finish_date = finish_date
|
||||||
|
except ParserError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not readthrough.start_date and not readthrough.finish_date:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return readthrough
|
53
bookwyrm/views/search.py
Normal file
53
bookwyrm/views/search.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
''' search views'''
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
|
from django.db.models.functions import Greatest
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.connectors import connector_manager
|
||||||
|
from bookwyrm.utils import regex
|
||||||
|
from .helpers import is_api_request
|
||||||
|
from .helpers import handle_remote_webfinger
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Search(View):
|
||||||
|
''' search users or books '''
|
||||||
|
def get(self, request):
|
||||||
|
''' that search bar up top '''
|
||||||
|
query = request.GET.get('q')
|
||||||
|
min_confidence = request.GET.get('min_confidence', 0.1)
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
# only return local book results via json so we don't cascade
|
||||||
|
book_results = connector_manager.local_search(
|
||||||
|
query, min_confidence=min_confidence)
|
||||||
|
return JsonResponse([r.json() for r in book_results], safe=False)
|
||||||
|
|
||||||
|
# use webfinger for mastodon style account@domain.com username
|
||||||
|
if re.match(r'\B%s' % regex.full_username, query):
|
||||||
|
handle_remote_webfinger(query)
|
||||||
|
|
||||||
|
# do a local user search
|
||||||
|
user_results = models.User.objects.annotate(
|
||||||
|
similarity=Greatest(
|
||||||
|
TrigramSimilarity('username', query),
|
||||||
|
TrigramSimilarity('localname', query),
|
||||||
|
)
|
||||||
|
).filter(
|
||||||
|
similarity__gt=0.5,
|
||||||
|
).order_by('-similarity')[:10]
|
||||||
|
|
||||||
|
book_results = connector_manager.search(
|
||||||
|
query, min_confidence=min_confidence)
|
||||||
|
data = {
|
||||||
|
'title': 'Search Results',
|
||||||
|
'book_results': book_results,
|
||||||
|
'user_results': user_results,
|
||||||
|
'query': query,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'search_results.html', data)
|
170
bookwyrm/views/shelf.py
Normal file
170
bookwyrm/views/shelf.py
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
''' shelf views'''
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from .helpers import is_api_request, get_edition, get_user_from_username
|
||||||
|
from .helpers import handle_reading_status
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Shelf(View):
|
||||||
|
''' shelf page '''
|
||||||
|
def get(self, request, username, shelf_identifier):
|
||||||
|
''' display a shelf '''
|
||||||
|
try:
|
||||||
|
user = get_user_from_username(username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
if shelf_identifier:
|
||||||
|
shelf = user.shelf_set.get(identifier=shelf_identifier)
|
||||||
|
else:
|
||||||
|
shelf = user.shelf_set.first()
|
||||||
|
|
||||||
|
is_self = request.user == user
|
||||||
|
|
||||||
|
shelves = user.shelf_set
|
||||||
|
if not is_self:
|
||||||
|
follower = user.followers.filter(id=request.user.id).exists()
|
||||||
|
# make sure the user has permission to view the shelf
|
||||||
|
if shelf.privacy == 'direct' or \
|
||||||
|
(shelf.privacy == 'followers' and not follower):
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
# only show other shelves that should be visible
|
||||||
|
if follower:
|
||||||
|
shelves = shelves.filter(privacy__in=['public', 'followers'])
|
||||||
|
else:
|
||||||
|
shelves = shelves.filter(privacy='public')
|
||||||
|
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
||||||
|
|
||||||
|
books = models.ShelfBook.objects.filter(
|
||||||
|
added_by=user, shelf=shelf
|
||||||
|
).order_by('-updated_date').all()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': '%s\'s %s shelf' % (user.display_name, shelf.name),
|
||||||
|
'user': user,
|
||||||
|
'is_self': is_self,
|
||||||
|
'shelves': shelves.all(),
|
||||||
|
'shelf': shelf,
|
||||||
|
'books': [b.book for b in books],
|
||||||
|
}
|
||||||
|
|
||||||
|
return TemplateResponse(request, 'shelf.html', data)
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
def post(self, request, username, shelf_id):
|
||||||
|
''' user generated shelves '''
|
||||||
|
if not request.user.username == username:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
shelf = get_object_or_404(models.Shelf, id=shelf_id)
|
||||||
|
if request.user != shelf.user:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
if not shelf.editable and request.POST.get('name') != shelf.name:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
form = forms.ShelfForm(request.POST, instance=shelf)
|
||||||
|
if not form.is_valid():
|
||||||
|
return redirect(shelf.local_path)
|
||||||
|
shelf = form.save()
|
||||||
|
return redirect(shelf.local_path)
|
||||||
|
|
||||||
|
|
||||||
|
def user_shelves_page(request, username):
|
||||||
|
''' default shelf '''
|
||||||
|
return Shelf.as_view()(request, username, None)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def create_shelf(request):
|
||||||
|
''' user generated shelves '''
|
||||||
|
form = forms.ShelfForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
shelf = form.save()
|
||||||
|
return redirect('/user/%s/shelf/%s' % \
|
||||||
|
(request.user.localname, shelf.identifier))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def delete_shelf(request, shelf_id):
|
||||||
|
''' user generated shelves '''
|
||||||
|
shelf = get_object_or_404(models.Shelf, id=shelf_id)
|
||||||
|
if request.user != shelf.user or not shelf.editable:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
shelf.delete()
|
||||||
|
return redirect('/user/%s/shelves' % request.user.localname)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def shelve(request):
|
||||||
|
''' put a on a user's shelf '''
|
||||||
|
book = get_edition(request.POST.get('book'))
|
||||||
|
|
||||||
|
desired_shelf = models.Shelf.objects.filter(
|
||||||
|
identifier=request.POST.get('shelf'),
|
||||||
|
user=request.user
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if request.POST.get('reshelve', True):
|
||||||
|
try:
|
||||||
|
current_shelf = models.Shelf.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
edition=book
|
||||||
|
)
|
||||||
|
handle_unshelve(request.user, book, current_shelf)
|
||||||
|
except models.Shelf.DoesNotExist:
|
||||||
|
# this just means it isn't currently on the user's shelves
|
||||||
|
pass
|
||||||
|
shelfbook = models.ShelfBook.objects.create(
|
||||||
|
book=book, shelf=desired_shelf, added_by=request.user)
|
||||||
|
broadcast(request.user, shelfbook.to_add_activity(request.user))
|
||||||
|
|
||||||
|
# post about "want to read" shelves
|
||||||
|
if desired_shelf.identifier == 'to-read':
|
||||||
|
handle_reading_status(
|
||||||
|
request.user,
|
||||||
|
desired_shelf,
|
||||||
|
book,
|
||||||
|
privacy=desired_shelf.privacy
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def unshelve(request):
|
||||||
|
''' put a on a user's shelf '''
|
||||||
|
book = models.Edition.objects.get(id=request.POST['book'])
|
||||||
|
current_shelf = models.Shelf.objects.get(id=request.POST['shelf'])
|
||||||
|
|
||||||
|
handle_unshelve(request.user, book, current_shelf)
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_unshelve(user, book, shelf):
|
||||||
|
''' unshelve a book '''
|
||||||
|
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
|
||||||
|
activity = row.to_remove_activity(user)
|
||||||
|
row.delete()
|
||||||
|
|
||||||
|
broadcast(user, activity)
|
195
bookwyrm/views/status.py
Normal file
195
bookwyrm/views/status.py
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
''' what are we here for if not for posting '''
|
||||||
|
import re
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from markdown import markdown
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from bookwyrm.sanitize_html import InputHtmlParser
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
from bookwyrm.status import create_notification, delete_status
|
||||||
|
from bookwyrm.utils import regex
|
||||||
|
from .helpers import get_user_from_username, handle_remote_webfinger
|
||||||
|
from .helpers import is_api_request, is_bookworm_request, status_visible_to_user
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Status(View):
|
||||||
|
''' the view for *posting* '''
|
||||||
|
def get(self, request, username, status_id):
|
||||||
|
''' display a particular status (and replies, etc) '''
|
||||||
|
try:
|
||||||
|
user = get_user_from_username(username)
|
||||||
|
status = models.Status.objects.select_subclasses().get(id=status_id)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
# the url should have the poster's username in it
|
||||||
|
if user != status.user:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
# make sure the user is authorized to see the status
|
||||||
|
if not status_visible_to_user(request.user, status):
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(
|
||||||
|
status.to_activity(pure=not is_bookworm_request(request)))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': 'Status by %s' % user.username,
|
||||||
|
'status': status,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'status.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class CreateStatus(View):
|
||||||
|
''' get posting '''
|
||||||
|
def post(self, request, status_type):
|
||||||
|
''' create status of whatever type '''
|
||||||
|
status_type = status_type[0].upper() + status_type[1:]
|
||||||
|
try:
|
||||||
|
form = getattr(forms, '%sForm' % status_type)(request.POST)
|
||||||
|
except AttributeError:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
if not form.is_valid():
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
status = form.save(commit=False)
|
||||||
|
if not status.sensitive and status.content_warning:
|
||||||
|
# the cw text field remains populated when you click "remove"
|
||||||
|
status.content_warning = None
|
||||||
|
status.save()
|
||||||
|
|
||||||
|
# inspect the text for user tags
|
||||||
|
content = status.content
|
||||||
|
for (mention_text, mention_user) in find_mentions(content):
|
||||||
|
# add them to status mentions fk
|
||||||
|
status.mention_users.add(mention_user)
|
||||||
|
|
||||||
|
# turn the mention into a link
|
||||||
|
content = re.sub(
|
||||||
|
r'%s([^@]|$)' % mention_text,
|
||||||
|
r'<a href="%s">%s</a>\g<1>' % \
|
||||||
|
(mention_user.remote_id, mention_text),
|
||||||
|
content)
|
||||||
|
|
||||||
|
# add reply parent to mentions and notify
|
||||||
|
if status.reply_parent:
|
||||||
|
status.mention_users.add(status.reply_parent.user)
|
||||||
|
for mention_user in status.reply_parent.mention_users.all():
|
||||||
|
status.mention_users.add(mention_user)
|
||||||
|
|
||||||
|
if status.reply_parent.user.local:
|
||||||
|
create_notification(
|
||||||
|
status.reply_parent.user,
|
||||||
|
'REPLY',
|
||||||
|
related_user=request.user,
|
||||||
|
related_status=status
|
||||||
|
)
|
||||||
|
|
||||||
|
# deduplicate mentions
|
||||||
|
status.mention_users.set(set(status.mention_users.all()))
|
||||||
|
# create mention notifications
|
||||||
|
for mention_user in status.mention_users.all():
|
||||||
|
if status.reply_parent and mention_user == status.reply_parent.user:
|
||||||
|
continue
|
||||||
|
if mention_user.local:
|
||||||
|
create_notification(
|
||||||
|
mention_user,
|
||||||
|
'MENTION',
|
||||||
|
related_user=request.user,
|
||||||
|
related_status=status
|
||||||
|
)
|
||||||
|
|
||||||
|
# don't apply formatting to generated notes
|
||||||
|
if not isinstance(status, models.GeneratedNote):
|
||||||
|
status.content = to_markdown(content)
|
||||||
|
# do apply formatting to quotes
|
||||||
|
if hasattr(status, 'quote'):
|
||||||
|
status.quote = to_markdown(status.quote)
|
||||||
|
|
||||||
|
status.save()
|
||||||
|
|
||||||
|
broadcast(
|
||||||
|
request.user,
|
||||||
|
status.to_create_activity(request.user),
|
||||||
|
software='bookwyrm')
|
||||||
|
|
||||||
|
# re-format the activity for non-bookwyrm servers
|
||||||
|
remote_activity = status.to_create_activity(request.user, pure=True)
|
||||||
|
broadcast(request.user, remote_activity, software='other')
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteStatus(View):
|
||||||
|
''' tombstone that bad boy '''
|
||||||
|
def post(self, request, status_id):
|
||||||
|
''' delete and tombstone a status '''
|
||||||
|
status = get_object_or_404(models.Status, id=status_id)
|
||||||
|
|
||||||
|
# don't let people delete other people's statuses
|
||||||
|
if status.user != request.user:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
# perform deletion
|
||||||
|
delete_status(status)
|
||||||
|
broadcast(request.user, status.to_delete_activity(request.user))
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
class Replies(View):
|
||||||
|
''' replies page (a json view of status) '''
|
||||||
|
def get(self, request, username, status_id):
|
||||||
|
''' ordered collection of replies to a status '''
|
||||||
|
# the html view is the same as Status
|
||||||
|
if not is_api_request(request):
|
||||||
|
status_view = Status.as_view()
|
||||||
|
return status_view(request, username, status_id)
|
||||||
|
|
||||||
|
# the json view is different than Status
|
||||||
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
if status.user.localname != username:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
return ActivitypubResponse(status.to_replies(**request.GET))
|
||||||
|
|
||||||
|
def find_mentions(content):
|
||||||
|
''' detect @mentions in raw status content '''
|
||||||
|
for match in re.finditer(regex.strict_username, content):
|
||||||
|
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)
|
||||||
|
|
||||||
|
mention_user = handle_remote_webfinger(username)
|
||||||
|
if not mention_user:
|
||||||
|
# we can ignore users we don't know about
|
||||||
|
continue
|
||||||
|
yield (match.group(), mention_user)
|
||||||
|
|
||||||
|
|
||||||
|
def format_links(content):
|
||||||
|
''' detect and format links '''
|
||||||
|
return re.sub(
|
||||||
|
r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
|
||||||
|
regex.domain,
|
||||||
|
r'\g<1><a href="\g<2>">\g<3></a>',
|
||||||
|
content)
|
||||||
|
|
||||||
|
def to_markdown(content):
|
||||||
|
''' catch links and convert to markdown '''
|
||||||
|
content = format_links(content)
|
||||||
|
content = markdown(content)
|
||||||
|
# sanitize resulting html
|
||||||
|
sanitizer = InputHtmlParser()
|
||||||
|
sanitizer.feed(content)
|
||||||
|
return sanitizer.get_output()
|
78
bookwyrm/views/tag.py
Normal file
78
bookwyrm/views/tag.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
''' tagging views'''
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponseNotFound
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from .helpers import is_api_request
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Tag(View):
|
||||||
|
''' tag page '''
|
||||||
|
def get(self, request, tag_id):
|
||||||
|
''' see books related to a tag '''
|
||||||
|
tag_obj = models.Tag.objects.filter(identifier=tag_id).first()
|
||||||
|
if not tag_obj:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(tag_obj.to_activity(**request.GET))
|
||||||
|
|
||||||
|
books = models.Edition.objects.filter(
|
||||||
|
usertag__tag__identifier=tag_id
|
||||||
|
).distinct()
|
||||||
|
data = {
|
||||||
|
'title': tag_obj.name,
|
||||||
|
'books': books,
|
||||||
|
'tag': tag_obj,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'tag.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class AddTag(View):
|
||||||
|
''' add a tag to a book '''
|
||||||
|
def post(self, request):
|
||||||
|
''' tag a book '''
|
||||||
|
# I'm not using a form here because sometimes "name" is sent as a hidden
|
||||||
|
# field which doesn't validate
|
||||||
|
name = request.POST.get('name')
|
||||||
|
book_id = request.POST.get('book')
|
||||||
|
book = get_object_or_404(models.Edition, id=book_id)
|
||||||
|
tag_obj, created = models.Tag.objects.get_or_create(
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
user_tag, _ = models.UserTag.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
book=book,
|
||||||
|
tag=tag_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
broadcast(request.user, user_tag.to_add_activity(request.user))
|
||||||
|
return redirect('/book/%s' % book_id)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class RemoveTag(View):
|
||||||
|
''' remove a user's tag from a book '''
|
||||||
|
def post(self, request):
|
||||||
|
''' untag a book '''
|
||||||
|
name = request.POST.get('name')
|
||||||
|
tag_obj = get_object_or_404(models.Tag, name=name)
|
||||||
|
book_id = request.POST.get('book')
|
||||||
|
book = get_object_or_404(models.Edition, id=book_id)
|
||||||
|
|
||||||
|
user_tag = get_object_or_404(
|
||||||
|
models.UserTag, tag=tag_obj, book=book, user=request.user)
|
||||||
|
tag_activity = user_tag.to_remove_activity(request.user)
|
||||||
|
user_tag.delete()
|
||||||
|
|
||||||
|
broadcast(request.user, tag_activity)
|
||||||
|
return redirect('/book/%s' % book_id)
|
181
bookwyrm/views/user.py
Normal file
181
bookwyrm/views/user.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
''' non-interactive pages '''
|
||||||
|
from io import BytesIO
|
||||||
|
from uuid import uuid4
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.http import HttpResponseNotFound
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
from .helpers import get_activity_feed, get_user_from_username, is_api_request
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class User(View):
|
||||||
|
''' user profile page '''
|
||||||
|
def get(self, request, username):
|
||||||
|
''' profile page for a user '''
|
||||||
|
try:
|
||||||
|
user = get_user_from_username(username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
# we have a json request
|
||||||
|
return ActivitypubResponse(user.to_activity())
|
||||||
|
# otherwise we're at a UI view
|
||||||
|
|
||||||
|
try:
|
||||||
|
page = int(request.GET.get('page', 1))
|
||||||
|
except ValueError:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
shelf_preview = []
|
||||||
|
|
||||||
|
# only show other shelves that should be visible
|
||||||
|
shelves = user.shelf_set
|
||||||
|
is_self = request.user.id == user.id
|
||||||
|
if not is_self:
|
||||||
|
follower = user.followers.filter(id=request.user.id).exists()
|
||||||
|
if follower:
|
||||||
|
shelves = shelves.filter(privacy__in=['public', 'followers'])
|
||||||
|
else:
|
||||||
|
shelves = shelves.filter(privacy='public')
|
||||||
|
|
||||||
|
for user_shelf in shelves.all():
|
||||||
|
if not user_shelf.books.count():
|
||||||
|
continue
|
||||||
|
shelf_preview.append({
|
||||||
|
'name': user_shelf.name,
|
||||||
|
'local_path': user_shelf.local_path,
|
||||||
|
'books': user_shelf.books.all()[:3],
|
||||||
|
'size': user_shelf.books.count(),
|
||||||
|
})
|
||||||
|
if len(shelf_preview) > 2:
|
||||||
|
break
|
||||||
|
|
||||||
|
# user's posts
|
||||||
|
activities = get_activity_feed(
|
||||||
|
request.user,
|
||||||
|
['public', 'unlisted', 'followers'],
|
||||||
|
queryset=models.Status.objects.filter(user=user)
|
||||||
|
)
|
||||||
|
paginated = Paginator(activities, PAGE_LENGTH)
|
||||||
|
data = {
|
||||||
|
'title': user.name,
|
||||||
|
'user': user,
|
||||||
|
'is_self': is_self,
|
||||||
|
'shelves': shelf_preview,
|
||||||
|
'shelf_count': shelves.count(),
|
||||||
|
'activities': paginated.page(page),
|
||||||
|
}
|
||||||
|
|
||||||
|
return TemplateResponse(request, 'user.html', data)
|
||||||
|
|
||||||
|
class Followers(View):
|
||||||
|
''' list of followers view '''
|
||||||
|
def get(self, request, username):
|
||||||
|
''' list of followers '''
|
||||||
|
try:
|
||||||
|
user = get_user_from_username(username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(
|
||||||
|
user.to_followers_activity(**request.GET))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': '%s: followers' % user.name,
|
||||||
|
'user': user,
|
||||||
|
'is_self': request.user.id == user.id,
|
||||||
|
'followers': user.followers.all(),
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'followers.html', data)
|
||||||
|
|
||||||
|
class Following(View):
|
||||||
|
''' list of following view '''
|
||||||
|
def get(self, request, username):
|
||||||
|
''' list of followers '''
|
||||||
|
try:
|
||||||
|
user = get_user_from_username(username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(
|
||||||
|
user.to_following_activity(**request.GET))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': '%s: following' % user.name,
|
||||||
|
'user': user,
|
||||||
|
'is_self': request.user.id == user.id,
|
||||||
|
'following': user.following.all(),
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'following.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class EditUser(View):
|
||||||
|
''' edit user view '''
|
||||||
|
def get(self, request):
|
||||||
|
''' profile page for a user '''
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
form = forms.EditUserForm(instance=request.user)
|
||||||
|
data = {
|
||||||
|
'title': 'Edit profile',
|
||||||
|
'form': form,
|
||||||
|
'user': user,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'edit_user.html', data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
''' les get fancy with images '''
|
||||||
|
form = forms.EditUserForm(
|
||||||
|
request.POST, request.FILES, instance=request.user)
|
||||||
|
if not form.is_valid():
|
||||||
|
data = {'form': form, 'user': request.user}
|
||||||
|
return TemplateResponse(request, 'edit_user.html', data)
|
||||||
|
|
||||||
|
user = form.save(commit=False)
|
||||||
|
|
||||||
|
if 'avatar' in form.files:
|
||||||
|
# crop and resize avatar upload
|
||||||
|
image = Image.open(form.files['avatar'])
|
||||||
|
target_size = 120
|
||||||
|
width, height = image.size
|
||||||
|
thumbnail_scale = height / (width / target_size) if height > width \
|
||||||
|
else width / (height / target_size)
|
||||||
|
image.thumbnail([thumbnail_scale, thumbnail_scale])
|
||||||
|
width, height = image.size
|
||||||
|
|
||||||
|
width_diff = width - target_size
|
||||||
|
height_diff = height - target_size
|
||||||
|
cropped = image.crop((
|
||||||
|
int(width_diff / 2),
|
||||||
|
int(height_diff / 2),
|
||||||
|
int(width - (width_diff / 2)),
|
||||||
|
int(height - (height_diff / 2))
|
||||||
|
))
|
||||||
|
output = BytesIO()
|
||||||
|
cropped.save(output, format=image.format)
|
||||||
|
ContentFile(output.getvalue())
|
||||||
|
|
||||||
|
# set the name to a hash
|
||||||
|
extension = form.files['avatar'].name.split('.')[-1]
|
||||||
|
filename = '%s.%s' % (uuid4(), extension)
|
||||||
|
user.avatar.save(filename, ContentFile(output.getvalue()))
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
broadcast(user, user.to_update_activity(user))
|
||||||
|
return redirect('/user/%s' % request.user.localname)
|
Loading…
Reference in a new issue