forked from mirrors/bookwyrm
Merge branch 'main' into progress_update
This commit is contained in:
commit
a4519d55c9
109 changed files with 5683 additions and 4458 deletions
|
@ -188,3 +188,8 @@ class ShelfForm(CustomForm):
|
|||
class Meta:
|
||||
model = models.Shelf
|
||||
fields = ['user', 'name', 'privacy']
|
||||
|
||||
class GoalForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.AnnualGoal
|
||||
fields = ['user', 'year', 'goal', 'privacy']
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
import csv
|
||||
import logging
|
||||
|
||||
from bookwyrm import outgoing
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm import models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.models import ImportJob, ImportItem
|
||||
from bookwyrm.status import create_notification
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -62,7 +63,7 @@ def import_data(job_id):
|
|||
item.save()
|
||||
|
||||
# shelves book and handles reviews
|
||||
outgoing.handle_imported_book(
|
||||
handle_imported_book(
|
||||
job.user, item, job.include_reviews, job.privacy)
|
||||
else:
|
||||
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)
|
||||
job.complete = True
|
||||
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
|
||||
import requests
|
||||
|
||||
from bookwyrm import activitypub, models, outgoing
|
||||
from bookwyrm import activitypub, models, views
|
||||
from bookwyrm import status as status_builder
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.signatures import Signature
|
||||
|
@ -133,7 +133,7 @@ def handle_follow(activity):
|
|||
related_user=relationship.user_subject
|
||||
)
|
||||
if not manually_approves:
|
||||
outgoing.handle_accept(relationship)
|
||||
views.handle_accept(relationship)
|
||||
|
||||
|
||||
@app.task
|
||||
|
|
|
@ -8,7 +8,6 @@ def set_rank(app_registry, schema_editor):
|
|||
db_alias = schema_editor.connection.alias
|
||||
books = app_registry.get_model('bookwyrm', 'Edition')
|
||||
for book in books.objects.using(db_alias):
|
||||
book.edition_rank = book.get_rank
|
||||
book.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
32
bookwyrm/migrations/0036_annualgoal.py
Normal file
32
bookwyrm/migrations/0036_annualgoal.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 3.0.7 on 2021-01-16 18:43
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0035_edition_edition_rank'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AnnualGoal',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
|
||||
('goal', models.IntegerField()),
|
||||
('year', models.IntegerField(default=2021)),
|
||||
('privacy', models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'year')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -17,7 +17,7 @@ from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
|
|||
|
||||
from .tag import Tag, UserTag
|
||||
|
||||
from .user import User, KeyPair
|
||||
from .user import User, KeyPair, AnnualGoal
|
||||
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
||||
from .federated_server import FederatedServer
|
||||
|
||||
|
|
|
@ -225,6 +225,9 @@ class OrderedCollectionPageMixin(ActivitypubMixin):
|
|||
def to_ordered_collection(self, queryset, \
|
||||
remote_id=None, page=False, **kwargs):
|
||||
''' an ordered collection of whatevers '''
|
||||
if not queryset.ordered:
|
||||
raise RuntimeError('queryset must be ordered')
|
||||
|
||||
remote_id = remote_id or self.remote_id
|
||||
if page:
|
||||
return to_ordered_collection_page(
|
||||
|
|
|
@ -182,7 +182,6 @@ class Edition(Book):
|
|||
activity_serializer = activitypub.Edition
|
||||
name_field = 'title'
|
||||
|
||||
@property
|
||||
def get_rank(self):
|
||||
''' calculate how complete the data is on this edition '''
|
||||
if self.parent_work and self.parent_work.default_edition == self:
|
||||
|
@ -208,7 +207,7 @@ class Edition(Book):
|
|||
self.isbn_13 = isbn_10_to_13(self.isbn_10)
|
||||
|
||||
# set rank
|
||||
self.edition_rank = self.get_rank
|
||||
self.edition_rank = self.get_rank()
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
@property
|
||||
def collection_queryset(self):
|
||||
''' list of books for this shelf, overrides OrderedCollectionMixin '''
|
||||
return self.books.all()
|
||||
return self.books.all().order_by('shelfbook')
|
||||
|
||||
def get_remote_id(self):
|
||||
''' shelf identifier instead of id '''
|
||||
|
@ -90,3 +90,4 @@ class ShelfBook(ActivitypubMixin, BookWyrmModel):
|
|||
''' an opinionated constraint!
|
||||
you can't put a book on shelf twice '''
|
||||
unique_together = ('book', 'shelf')
|
||||
ordering = ('-created_date',)
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.apps import apps
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.connectors import get_data
|
||||
|
@ -18,7 +19,7 @@ from bookwyrm.utils import regex
|
|||
from .base_model import OrderedCollectionPageMixin
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from .federated_server import FederatedServer
|
||||
from . import fields
|
||||
from . import fields, Review
|
||||
|
||||
|
||||
class User(OrderedCollectionPageMixin, AbstractUser):
|
||||
|
@ -221,6 +222,57 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
|
|||
return activity_object
|
||||
|
||||
|
||||
class AnnualGoal(BookWyrmModel):
|
||||
''' set a goal for how many books you read in a year '''
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
goal = models.IntegerField()
|
||||
year = models.IntegerField(default=timezone.now().year)
|
||||
privacy = models.CharField(
|
||||
max_length=255,
|
||||
default='public',
|
||||
choices=fields.PrivacyLevels.choices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
''' unqiueness constraint '''
|
||||
unique_together = ('user', 'year')
|
||||
|
||||
def get_remote_id(self):
|
||||
''' put the year in the path '''
|
||||
return '%s/goal/%d' % (self.user.remote_id, self.year)
|
||||
|
||||
@property
|
||||
def books(self):
|
||||
''' the books you've read this year '''
|
||||
return self.user.readthrough_set.filter(
|
||||
finish_date__year__gte=self.year
|
||||
).order_by('finish_date').all()
|
||||
|
||||
|
||||
@property
|
||||
def ratings(self):
|
||||
''' ratings for books read this year '''
|
||||
book_ids = [r.book.id for r in self.books]
|
||||
reviews = Review.objects.filter(
|
||||
user=self.user,
|
||||
book__in=book_ids,
|
||||
)
|
||||
return {r.book.id: r.rating for r in reviews}
|
||||
|
||||
|
||||
@property
|
||||
def progress_percent(self):
|
||||
return int(float(self.book_count / self.goal) * 100)
|
||||
|
||||
|
||||
@property
|
||||
def book_count(self):
|
||||
''' how many books you've read this year '''
|
||||
return self.user.readthrough_set.filter(
|
||||
finish_date__year__gte=self.year).count()
|
||||
|
||||
|
||||
|
||||
@receiver(models.signals.post_save, sender=User)
|
||||
#pylint: disable=unused-argument
|
||||
def execute_after_save(sender, instance, created, *args, **kwargs):
|
||||
|
|
|
@ -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()
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
Binary file not shown.
Binary file not shown.
|
@ -9,6 +9,9 @@
|
|||
.card {
|
||||
overflow: visible;
|
||||
}
|
||||
.card-header-title {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- TOGGLES --- */
|
||||
input.toggle-control {
|
||||
|
@ -145,11 +148,11 @@ input.toggle-control:checked ~ .modal.toggle-content {
|
|||
position: absolute;
|
||||
}
|
||||
.quote blockquote:before {
|
||||
content: "\e905";
|
||||
content: "\e906";
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.quote blockquote:after {
|
||||
content: "\e904";
|
||||
content: "\e905";
|
||||
right: 0;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('fonts/icomoon.eot?rd4abb');
|
||||
src: url('fonts/icomoon.eot?rd4abb#iefix') format('embedded-opentype'),
|
||||
url('fonts/icomoon.ttf?rd4abb') format('truetype'),
|
||||
url('fonts/icomoon.woff?rd4abb') format('woff'),
|
||||
url('fonts/icomoon.svg?rd4abb#icomoon') format('svg');
|
||||
src: url('fonts/icomoon.eot?uh765c');
|
||||
src: url('fonts/icomoon.eot?uh765c#iefix') format('embedded-opentype'),
|
||||
url('fonts/icomoon.ttf?uh765c') format('truetype'),
|
||||
url('fonts/icomoon.woff?uh765c') format('woff'),
|
||||
url('fonts/icomoon.svg?uh765c#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@ -25,81 +25,102 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-dots-three-vertical:before {
|
||||
content: "\e918";
|
||||
.icon-sparkle:before {
|
||||
content: "\e91a";
|
||||
}
|
||||
.icon-check:before {
|
||||
content: "\e917";
|
||||
.icon-warning:before {
|
||||
content: "\e91b";
|
||||
}
|
||||
.icon-dots-three:before {
|
||||
content: "\e916";
|
||||
}
|
||||
.icon-envelope:before {
|
||||
.icon-book:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-arrow-right:before {
|
||||
.icon-bookmark:before {
|
||||
content: "\e91c";
|
||||
}
|
||||
.icon-envelope:before {
|
||||
content: "\e901";
|
||||
}
|
||||
.icon-bell:before {
|
||||
.icon-arrow-right:before {
|
||||
content: "\e902";
|
||||
}
|
||||
.icon-x:before {
|
||||
.icon-bell:before {
|
||||
content: "\e903";
|
||||
}
|
||||
.icon-quote-close:before {
|
||||
.icon-x:before {
|
||||
content: "\e904";
|
||||
}
|
||||
.icon-quote-open:before {
|
||||
.icon-quote-close:before {
|
||||
content: "\e905";
|
||||
}
|
||||
.icon-image:before {
|
||||
.icon-quote-open:before {
|
||||
content: "\e906";
|
||||
}
|
||||
.icon-pencil:before {
|
||||
.icon-image:before {
|
||||
content: "\e907";
|
||||
}
|
||||
.icon-list:before {
|
||||
.icon-pencil:before {
|
||||
content: "\e908";
|
||||
}
|
||||
.icon-unlock:before {
|
||||
.icon-list:before {
|
||||
content: "\e909";
|
||||
}
|
||||
.icon-globe:before {
|
||||
.icon-unlock:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
.icon-lock:before {
|
||||
.icon-unlisted:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
.icon-globe:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon-chain-broken:before {
|
||||
.icon-public:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon-lock:before {
|
||||
content: "\e90c";
|
||||
}
|
||||
.icon-chain:before {
|
||||
.icon-followers:before {
|
||||
content: "\e90c";
|
||||
}
|
||||
.icon-chain-broken:before {
|
||||
content: "\e90d";
|
||||
}
|
||||
.icon-comments:before {
|
||||
.icon-chain:before {
|
||||
content: "\e90e";
|
||||
}
|
||||
.icon-comment:before {
|
||||
.icon-comments:before {
|
||||
content: "\e90f";
|
||||
}
|
||||
.icon-boost:before {
|
||||
.icon-comment:before {
|
||||
content: "\e910";
|
||||
}
|
||||
.icon-arrow-left:before {
|
||||
.icon-boost:before {
|
||||
content: "\e911";
|
||||
}
|
||||
.icon-arrow-up:before {
|
||||
.icon-arrow-left:before {
|
||||
content: "\e912";
|
||||
}
|
||||
.icon-arrow-down:before {
|
||||
.icon-arrow-up:before {
|
||||
content: "\e913";
|
||||
}
|
||||
.icon-home:before {
|
||||
.icon-arrow-down:before {
|
||||
content: "\e914";
|
||||
}
|
||||
.icon-local:before {
|
||||
.icon-home:before {
|
||||
content: "\e915";
|
||||
}
|
||||
.icon-local:before {
|
||||
content: "\e916";
|
||||
}
|
||||
.icon-dots-three:before {
|
||||
content: "\e917";
|
||||
}
|
||||
.icon-check:before {
|
||||
content: "\e918";
|
||||
}
|
||||
.icon-dots-three-vertical:before {
|
||||
content: "\e919";
|
||||
}
|
||||
.icon-search:before {
|
||||
content: "\e986";
|
||||
}
|
||||
|
|
|
@ -1,3 +1,64 @@
|
|||
// set up javascript listeners
|
||||
window.onload = function() {
|
||||
// let buttons set keyboard focus
|
||||
Array.from(document.getElementsByClassName('toggle-control'))
|
||||
.forEach(t => t.onclick = toggleAction);
|
||||
|
||||
// javascript interactions (boost/fav)
|
||||
Array.from(document.getElementsByClassName('interaction'))
|
||||
.forEach(t => t.onsubmit = interact);
|
||||
|
||||
// select all
|
||||
Array.from(document.getElementsByClassName('select-all'))
|
||||
.forEach(t => t.onclick = selectAll);
|
||||
|
||||
// toggle between tabs
|
||||
Array.from(document.getElementsByClassName('tab-change-nested'))
|
||||
.forEach(t => t.onclick = tabChangeNested);
|
||||
Array.from(document.getElementsByClassName('tab-change'))
|
||||
.forEach(t => t.onclick = tabChange);
|
||||
|
||||
// handle aria settings on menus
|
||||
Array.from(document.getElementsByClassName('pulldown-menu'))
|
||||
.forEach(t => t.onclick = toggleMenu);
|
||||
|
||||
// display based on localstorage vars
|
||||
document.querySelectorAll('[data-hide]')
|
||||
.forEach(t => setDisplay(t));
|
||||
|
||||
// update localstorage
|
||||
Array.from(document.getElementsByClassName('set-display'))
|
||||
.forEach(t => t.onclick = updateDisplay);
|
||||
};
|
||||
|
||||
function updateDisplay(e) {
|
||||
var key = e.target.getAttribute('data-id');
|
||||
var value = e.target.getAttribute('data-value');
|
||||
window.localStorage.setItem(key, value);
|
||||
|
||||
document.querySelectorAll('[data-hide="' + key + '"]')
|
||||
.forEach(t => setDisplay(t));
|
||||
}
|
||||
|
||||
function setDisplay(el) {
|
||||
var key = el.getAttribute('data-hide');
|
||||
var value = window.localStorage.getItem(key)
|
||||
if (!value) {
|
||||
el.className = el.className.replace('hidden', '');
|
||||
} else if (value != null && !!value) {
|
||||
el.className += ' hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAction(e) {
|
||||
// set hover, if appropriate
|
||||
var hover = e.target.getAttribute('data-hover-target');
|
||||
if (hover) {
|
||||
document.getElementById(hover).focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function interact(e) {
|
||||
e.preventDefault();
|
||||
ajaxPost(e.target);
|
||||
|
@ -13,49 +74,36 @@ function interact(e) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function reply(e) {
|
||||
e.preventDefault();
|
||||
ajaxPost(e.target);
|
||||
// TODO: display comment
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectAll(el) {
|
||||
el.parentElement.querySelectorAll('[type="checkbox"]')
|
||||
function selectAll(e) {
|
||||
e.target.parentElement.parentElement.querySelectorAll('[type="checkbox"]')
|
||||
.forEach(t => t.checked=true);
|
||||
}
|
||||
|
||||
function rate_stars(e) {
|
||||
e.preventDefault();
|
||||
ajaxPost(e.target);
|
||||
rating = e.target.rating.value;
|
||||
var stars = e.target.parentElement.getElementsByClassName('icon');
|
||||
for (var i = 0; i < stars.length ; i++) {
|
||||
stars[i].className = rating > i ? 'icon icon-star-full' : 'icon icon-star-empty';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function tabChange(e, nested) {
|
||||
function tabChangeNested(e) {
|
||||
var target = e.target.closest('li')
|
||||
var identifier = target.getAttribute('data-id');
|
||||
|
||||
if (nested) {
|
||||
var parent_element = target.parentElement.closest('li').parentElement;
|
||||
} else {
|
||||
var parent_element = target.parentElement;
|
||||
var parentElement = target.parentElement.closest('li').parentElement;
|
||||
handleTabChange(target, parentElement)
|
||||
}
|
||||
|
||||
parent_element.querySelectorAll('[aria-selected="true"]')
|
||||
function tabChange(e) {
|
||||
var target = e.target.closest('li')
|
||||
var parentElement = target.parentElement;
|
||||
handleTabChange(target, parentElement)
|
||||
}
|
||||
|
||||
|
||||
function handleTabChange(target, parentElement) {
|
||||
parentElement.querySelectorAll('[aria-selected="true"]')
|
||||
.forEach(t => t.setAttribute("aria-selected", false));
|
||||
target.querySelector('[role="tab"]').setAttribute("aria-selected", true);
|
||||
|
||||
parent_element.querySelectorAll('li')
|
||||
parentElement.querySelectorAll('li')
|
||||
.forEach(t => t.className='');
|
||||
target.className = 'is-active';
|
||||
}
|
||||
|
||||
function toggleMenu(el) {
|
||||
function toggleMenu(e) {
|
||||
var el = e.target.closest('.pulldown-menu');
|
||||
el.setAttribute('aria-expanded', el.getAttribute('aria-expanded') == 'false');
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ author.local_path }}/edit">
|
||||
<span class="icon icon-pencil">
|
||||
<span class="icon icon-pencil" title="Edit Author">
|
||||
<span class="is-sr-only">Edit Author</span>
|
||||
</span>
|
||||
</a>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ book.id }}/edit">
|
||||
<span class="icon icon-pencil">
|
||||
<span class="icon icon-pencil" title="Edit Book">
|
||||
<span class="is-sr-only">Edit Book</span>
|
||||
</span>
|
||||
</a>
|
||||
|
@ -95,14 +95,14 @@
|
|||
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book and not book|book_description %}
|
||||
<div>
|
||||
<input class="toggle-control" type="radio" name="add-description" id="hide-description" checked>
|
||||
<input class="toggle-control" type="radio" name="add-description" id="hide-description-{{ book.id }}" checked>
|
||||
<div class="toggle-content hidden">
|
||||
<label class="button" for="add-description" tabindex="0" role="button">Add description</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Add description" controls_text="add-description" controls_uid=book.id %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input class="toggle-control" type="radio" name="add-description" id="add-description">
|
||||
<input class="toggle-control" type="radio" name="add-description" id="add-description-{{ book.id }}" data-hover-target="id_description">
|
||||
<div class="toggle-content hidden">
|
||||
<div class="box">
|
||||
<form name="add-description" method="POST" action="/add-description/{{ book.id }}">
|
||||
|
@ -113,7 +113,7 @@
|
|||
</p>
|
||||
<div class="field">
|
||||
<button class="button is-primary" type="submit">Save</button>
|
||||
<label class="button" for="hide-description" tabindex="0" role="button">Cancel</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="hide-description" controls_uid=book.id %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -155,7 +155,7 @@
|
|||
<div>
|
||||
<input type="radio" class="toggle-control" name="add-readthrough-form" id="hide-create-readthrough" checked>
|
||||
<div class="toggle-content hidden">
|
||||
<label class="button" for="add-readthrough" class="button" role="button" tabindex="0">Add read dates</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Add read dates" controls_text="add-readthrough" %}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -165,7 +165,7 @@
|
|||
{% include 'snippets/readthrough_form.html' with readthrough=None %}
|
||||
<div class="field is-grouped">
|
||||
<button class="button is-primary" type="submit">Create</button>
|
||||
<label class="button" for="hide-create-readthrough" role="button" tabindex="0">Cancel</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="hide-create-readthrough" %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -223,7 +223,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="block" id="reviews">
|
||||
{% for review in reviews %}
|
||||
<div class="block">
|
||||
{% include 'snippets/status.html' with status=review hide_book=True depth=1 %}
|
||||
|
@ -251,6 +251,9 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="block">
|
||||
{% include 'snippets/pagination.html' with page=reviews path=book.local_path anchor="#reviews" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,25 +13,7 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||
{% 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>
|
||||
{% include 'snippets/pagination.html' with page=activities path="direct-messages" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="tile is-child box has-background-primary-light content">
|
||||
{% if site.allow_registration %}
|
||||
<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' %}
|
||||
</form>
|
||||
{% else %}
|
||||
|
|
|
@ -1,25 +1,16 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load humanize %}
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<div class="level">
|
||||
<header class="block">
|
||||
<h1 class="title level-left">
|
||||
Edit "{{ author.name }}"
|
||||
</h1>
|
||||
<div class="level-right">
|
||||
<a href="/author/{{ author.id }}">
|
||||
<span class="edit-link icon icon-close">
|
||||
<span class="is-sr-only">Close</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Added: {{ author.created_date | naturaltime }}</p>
|
||||
<p>Updated: {{ author.updated_date | naturaltime }}</p>
|
||||
<p>Last edited by: <a href="{{ author.last_edited_by.remote_id }}">{{ author.last_edited_by.display_name }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="block">
|
||||
|
@ -27,7 +18,7 @@
|
|||
</div>
|
||||
{% 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 %}
|
||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||
|
||||
|
|
|
@ -1,25 +1,16 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load humanize %}
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<div class="level">
|
||||
<header class="block">
|
||||
<h1 class="title level-left">
|
||||
Edit "{{ book.title }}"
|
||||
</h1>
|
||||
<div class="level-right">
|
||||
<a href="/book/{{ book.id }}">
|
||||
<span class="edit-link icon icon-close">
|
||||
<span class="is-sr-only">Close</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Added: {{ book.created_date | naturaltime }}</p>
|
||||
<p>Updated: {{ book.updated_date | naturaltime }}</p>
|
||||
<p>Last edited by: <a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="block">
|
||||
|
@ -27,7 +18,7 @@
|
|||
</div>
|
||||
{% 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 %}
|
||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||
<div class="columns">
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<ul>
|
||||
{% for book in shelf.books %}
|
||||
<li class="{% if shelf_counter == 1 and forloop.first %}is-active{% endif %}" data-id="tab-book-{{ book.id }}">
|
||||
<label for="book-{{ book.id }}" onclick="tabChange(event, nested=true)">
|
||||
<label for="book-{{ book.id }}" class="tab-change-nested">
|
||||
<div role="tab" tabindex="0" aria-selected="{% if shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}" aria-controls="book-{{ book.id }}-panel">
|
||||
<a>
|
||||
{% include 'snippets/book_cover.html' with book=book size="medium" %}
|
||||
|
@ -48,9 +48,9 @@
|
|||
<div class="card-header">
|
||||
<p class="card-header-title">
|
||||
<span>{% include 'snippets/book_titleby.html' with book=book %}</span>
|
||||
</>
|
||||
</p>
|
||||
<div class="card-header-icon is-hidden-tablet">
|
||||
<label class="delete" for="no-book" aria-label="close" role="button"></label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with label="close" controls_text="no-book" class="delete" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
|
@ -77,6 +77,15 @@
|
|||
<input class="toggle-control" type="radio" name="recent-books" id="no-book">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if goal %}
|
||||
<section class="section">
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{{ goal.year }} Reading Goal</h3>
|
||||
{% include 'snippets/goal_progress.html' with goal=goal %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="column is-two-thirds" id="feed">
|
||||
|
@ -95,6 +104,33 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
{# announcements and system messages #}
|
||||
{% if not goal and tab == 'home' %}
|
||||
{% now 'Y' as year %}
|
||||
<section class="block hidden" aria-title="Announcements" data-hide="hide-{{ year }}-reading-goal">
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<h3 class="card-header-title has-background-primary has-text-white">
|
||||
<span class="icon icon-book is-size-3 mr-2" aria-hidden="true"></span> {{ year }} reading goal
|
||||
</h3>
|
||||
</header>
|
||||
<section class="card-content content">
|
||||
<p>Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.</p>
|
||||
|
||||
{% include 'snippets/goal_form.html' %}
|
||||
</section>
|
||||
<footer class="card-footer has-background-white-bis">
|
||||
<div class="card-footer-item is-flex-direction-column">
|
||||
<button class="button is-danger is-light is-block set-display" data-id="hide-{{ year }}-reading-goal" data-value="true">Dismiss message</button>
|
||||
<p class="help">You can set or change your reading goal any time from your <a href="{{ request.user.local_path }}">profile page</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
<hr>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# activity feed #}
|
||||
{% if not activities %}
|
||||
<p>There aren't any activities right now! Try following a user to get started</p>
|
||||
{% endif %}
|
||||
|
@ -104,25 +140,7 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||
{% 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>
|
||||
{% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
60
bookwyrm/templates/goal.html
Normal file
60
bookwyrm/templates/goal.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block content %}
|
||||
|
||||
<section class="block">
|
||||
<h1 class="title">{{ year }} Reading Progress</h1>
|
||||
{% if user == request.user %}
|
||||
<div class="block">
|
||||
{% if goal %}
|
||||
<input type="radio" class="toggle-control" name="edit-goal" id="hide-edit-goal" checked>
|
||||
<div class="toggle-content hidden">
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Edit goal" controls_text="show-edit-goal" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="block">
|
||||
<input type="radio" class="toggle-control" name="edit-goal" id="show-edit-goal" data-hover-target="edit-form-header">
|
||||
<div class="toggle-content{% if goal %} hidden{% endif %}">
|
||||
{% now 'Y' as year %}
|
||||
<section class="card">
|
||||
<header class="card-header">
|
||||
<h2 class="card-header-title has-background-primary has-text-white" tabindex="0" id="edit-form-header">
|
||||
<span class="icon icon-book is-size-3 mr-2" aria-hidden="true"></span> {{ year }} reading goal
|
||||
</h2>
|
||||
</header>
|
||||
<section class="card-content content">
|
||||
<p>Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.</p>
|
||||
|
||||
{% include 'snippets/goal_form.html' with goal=goal year=year %}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not goal and user != request.user %}
|
||||
<p>{{ user.display_name }} hasn't set a reading goal for {{ year }}.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if goal %}
|
||||
{% include 'snippets/goal_progress.html' with goal=goal %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if goal.books %}
|
||||
<section>
|
||||
<h2 class="title">{% if goal.user == request.user %}Your{% else %}{{ goal.user.display_name }}'s{% endif %} {{ year }} Books</h2>
|
||||
<div class="columns is-multiline">
|
||||
{% for book in goal.books %}
|
||||
<div class="column is-narrow">
|
||||
<div class="box">
|
||||
<a href="{{ book.book.local_path }}">
|
||||
{% include 'snippets/discover/small-book.html' with book=book.book rating=goal.ratings %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -3,7 +3,7 @@
|
|||
{% block content %}
|
||||
<div class="block">
|
||||
<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 %}
|
||||
<div class="field">
|
||||
{{ import_form.as_p }}
|
||||
|
@ -30,7 +30,7 @@
|
|||
{% endif %}
|
||||
<ul>
|
||||
{% 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 %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -30,9 +30,8 @@
|
|||
<div class="block">
|
||||
<h2 class="title is-4">Failed to load</h2>
|
||||
{% if not job.retry %}
|
||||
<form name="retry" action="/retry-import/" method="post">
|
||||
<form name="retry" action="/import/{{ job.id }}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="import_job" value="{{ job.id }}">
|
||||
<ul>
|
||||
<fieldset>
|
||||
{% for item in failed_items %}
|
||||
|
@ -50,7 +49,7 @@
|
|||
{% endfor %}
|
||||
</fieldset>
|
||||
</ul>
|
||||
<div class="block pt-1" onclick="selectAll(this)">
|
||||
<div class="block pt-1 select-all">
|
||||
<label class="label">
|
||||
<input type="checkbox" class="checkbox">
|
||||
Select all
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{% if valid %}
|
||||
<h1 class="title">Create an Account</h1>
|
||||
<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 }}">
|
||||
{% include 'snippets/register_form.html' %}
|
||||
</form>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<div class="control">
|
||||
<button class="button" type="submit">
|
||||
<span class="icon icon-search">
|
||||
<span class="icon icon-search" title="Search">
|
||||
<span class="is-sr-only">search</span>
|
||||
</span>
|
||||
</button>
|
||||
|
@ -41,9 +41,9 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<label for="main-nav" role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="mainNav" onclick="toggleMenu(this)" tabindex="0">
|
||||
<label for="main-nav" role="button" class="navbar-burger pulldown-menu" aria-label="menu" aria-expanded="false" data-target="mainNav" tabindex="0">
|
||||
<div class="navbar-item mt-3">
|
||||
<div class="icon icon-dots-three-vertical">
|
||||
<div class="icon icon-dots-three-vertical" title="Main navigation menu">
|
||||
<span class="is-sr-only">Main navigation menu</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,7 +66,7 @@
|
|||
<div class="navbar-end">
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<div class="navbar-link" role="button" aria-expanded=false" onclick="toggleMenu(this)" tabindex="0" aria-haspopup="true" aria-controls="navbar-dropdown"><p>
|
||||
<div class="navbar-link pulldown-menu" role="button" aria-expanded="false" tabindex="0" aria-haspopup="true" aria-controls="navbar-dropdown"><p>
|
||||
{% include 'snippets/avatar.html' with user=request.user %}
|
||||
{% include 'snippets/username.html' with user=request.user %}
|
||||
</p></div>
|
||||
|
@ -82,7 +82,7 @@
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-edit" class="navbar-item">
|
||||
<a href="/edit-profile" class="navbar-item">
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
|
@ -110,7 +110,7 @@
|
|||
<a href="/notifications">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-medium">
|
||||
<span class="icon icon-bell">
|
||||
<span class="icon icon-bell" title="Notifications">
|
||||
<span class="is-sr-only">Notifications</span>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -122,23 +122,25 @@
|
|||
</div>
|
||||
{% else %}
|
||||
<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="column">
|
||||
<form name="login" method="post" action="/user-login">
|
||||
<form name="login" method="post" action="/login">
|
||||
{% csrf_token %}
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<div class="columns is-variable is-1">
|
||||
<div class="column">
|
||||
<label class="is-sr-only" for="id_localname">Username:</label>
|
||||
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname" placeholder="username">
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="column">
|
||||
<label class="is-sr-only" for="id_password">Username:</label>
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="password">
|
||||
<p class="help"><a href="/password-reset">Forgot your password?</a></p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<button class="button is-primary" type="submit">Log in</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if site.allow_registration and request.path != '' and request.path != '/' %}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% if login_form.non_field_errors %}
|
||||
<p class="notification is-danger">{{ login_form.non_field_errors }}</p>
|
||||
{% endif %}
|
||||
<form name="login" method="post" action="/user-login">
|
||||
<form name="login" method="post" action="/login">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_localname">Username:</label>
|
||||
|
@ -38,7 +38,7 @@
|
|||
<div class="box has-background-primary-light">
|
||||
{% if site.allow_registration %}
|
||||
<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' %}
|
||||
</form>
|
||||
{% else %}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<div class="block">
|
||||
<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 %}
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="block">
|
||||
<h1 class="title">Notifications</h1>
|
||||
|
||||
<form name="clear" action="/clear-notifications" method="POST">
|
||||
<form name="clear" action="/notifications" method="POST">
|
||||
{% csrf_token %}
|
||||
<button class="button is-danger is-light" type="submit" class="secondary">Delete notifications</button>
|
||||
</form>
|
||||
|
@ -63,7 +63,7 @@
|
|||
boosted your <a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -8,9 +8,8 @@
|
|||
{% for error in errors %}
|
||||
<p class="is-danger">{{ error }}</p>
|
||||
{% endfor %}
|
||||
<form name="reset-password" method="post" action="/reset-password">
|
||||
<form name="password-reset" method="post" action="/password-reset/{{ code }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="reset-code" value="{{ code }}">
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">Password:</label>
|
||||
<div class="control">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<h1 class="title">Reset Password</h1>
|
||||
{% if message %}<p>{{ message }}</p>{% endif %}
|
||||
<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 %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_email_register">Email address:</label>
|
||||
|
|
|
@ -30,9 +30,7 @@
|
|||
|
||||
<input class="toggle-control" type="radio" name="more-results" id="fewer-results" checked>
|
||||
<div class="toggle-content hidden">
|
||||
<label class="button is-small" for="more-results">
|
||||
<div role="button" tabindex="0">Show results from other catalogues</div>
|
||||
</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Show results from other catalogues" small=True controls_text="more-results" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -64,9 +62,7 @@
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if local_results.results %}
|
||||
<label class="button is-small" for="fewer-results">
|
||||
<div role="button" tabindex="0">Hide results from other catalogues</div>
|
||||
</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Hide results from other catalogues" small=True controls_text="fewer-results" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -33,18 +33,13 @@
|
|||
<div class="column is-narrow">
|
||||
<input type="radio" id="create-shelf-form-hide" name="create-shelf-form" class="toggle-control" checked>
|
||||
<div class="toggle-content hidden">
|
||||
<label for="create-shelf-form-show">
|
||||
<div role="button" tabindex="0">
|
||||
<span class="icon icon-plus">
|
||||
<span class="is-sr-only">Create new shelf</span>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Create shelf" icon="plus" class="is-clickable" controls_text="create-shelf-form-show" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="radio" id="create-shelf-form-show" name="create-shelf-form" class="toggle-control">
|
||||
<div class="toggle-content hidden">
|
||||
<div class="box mb-5">
|
||||
|
@ -63,11 +58,12 @@
|
|||
</label>
|
||||
<div class="field is-grouped">
|
||||
<button class="button is-primary" type="submit">Create shelf</button>
|
||||
<label role="button" class="button" for="create-shelf-form-hide" tabindex="0">Cancel<label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="create-shelf-form-hide" %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block columns">
|
||||
<div class="column">
|
||||
|
@ -82,13 +78,7 @@
|
|||
<div class="column is-narrow">
|
||||
<input type="radio" id="edit-shelf-form-hide" name="edit-shelf-form" class="toggle-control" checked>
|
||||
<div class="toggle-content hidden">
|
||||
<label for="edit-shelf-form-show">
|
||||
<div role="button" tabindex="0">
|
||||
<span class="icon icon-pencil">
|
||||
<span class="is-sr-only">Edit shelf</span>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Edit shelf" icon="pencil" class="is-clickable" controls_text="edit-shelf-form-show" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -116,7 +106,7 @@
|
|||
</label>
|
||||
<div class="field is-grouped">
|
||||
<button class="button is-primary" type="submit">Update shelf</button>
|
||||
<label role="button" class="button" for="edit-shelf-form-hide" tabindex="0">Cancel<label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="edit-shelf-form-hide" %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
{% load bookwyrm_tags %}
|
||||
|
||||
{% with status.id|uuid as uuid %}
|
||||
<form name="boost" action="/boost/{{ status.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
|
||||
<form name="boost" action="/boost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-small" type="submit" {% if not status.boostable %}disabled{% endif %}>
|
||||
<span class="icon icon-boost">
|
||||
<span class="icon icon-boost" title="Boost status">
|
||||
<span class="is-sr-only">Boost status</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<form name="unboost" action="/unboost/{{ status.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
|
||||
<form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-small is-success" type="submit">
|
||||
<span class="icon icon-boost">
|
||||
<span class="icon icon-boost" title="Un-boost status">
|
||||
<span class="is-sr-only">Un-boost status</span>
|
||||
</span>
|
||||
</button>
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
{% with 0|uuid as uuid %}
|
||||
<div class="control">
|
||||
<div>
|
||||
<input type="radio" class="toggle-control" name="sensitive" value="false" id="hide-spoilers-{{ uuid }}" {% if not parent_status.content_warning %}checked{% endif %}>
|
||||
<input type="radio" class="toggle-control" id="include-spoilers-{{ uuid }}" name="sensitive" value="true" {% if parent_status.content_warning %}checked{% endif %} data-hover-target="id_content_warning_{{ uuid }}">
|
||||
<div class="toggle-content hidden">
|
||||
<label class="button is-small" role="button" tabindex="0" for="include-spoilers-{{ uuid }}">Add spoilers/content warning</label>
|
||||
<label class="is-sr-only" for="id_content_warning_{{ uuid }}">Spoilers/content warning:</label>
|
||||
<input type="text" name="content_warning" maxlength="255" class="input" id="id_content_warning_{{ uuid }}" placeholder="Spoilers ahead!"{% if parent_status.content_warning %} value="{{ parent_status.content_warning }}"{% endif %}>
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="hide-spoilers" controls_uid=uuid text="Remove spoilers/content warning" small=True %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="radio" class="toggle-control" id="include-spoilers-{{ uuid }}" name="sensitive" value="true" {% if parent_status.content_warning %}checked{% endif %}>
|
||||
<input type="radio" class="toggle-control" name="sensitive" value="false" id="hide-spoilers-{{ uuid }}" {% if not parent_status.content_warning %}checked{% endif %}>
|
||||
<div class="toggle-content hidden">
|
||||
<label class="button is-small" role="button" tabindex="0" for="hide-spoilers-{{ uuid }}">Remove spoilers/content warning</label>
|
||||
<label class="is-sr-only" for="id_content_warning_{{ uuid }}">Spoilers/content warning:</label>
|
||||
<input type="text" name="content_warning" maxlength="255" class="input" id="id_content_warning_{{ uuid }}" placeholder="Spoilers ahead!"{% if parent_status.content_warning %} value="{{ parent_status.content_warning }}"{% endif %}>
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="include-spoilers" controls_uid=uuid text="Add spoilers/content warning" small=True %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,21 +5,21 @@
|
|||
<ul role="tablist">
|
||||
<li class="is-active" data-id="tab-review-{{ book.id }}" data-category="tab-option-{{ book.id }}">
|
||||
<label for="review-{{ book.id }}">
|
||||
<div onclick="tabChange(event)" role="tab" aria-selected="true" tabindex="0">
|
||||
<div class="tab-change" role="tab" aria-selected="true" tabindex="0">
|
||||
<a>Review</a>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
<li data-id="tab-comment-{{ book.id }}" data-category="tab-option-{{ book.id }}">
|
||||
<label for="comment-{{ book.id}}">
|
||||
<div onclick="tabChange(event)" role="tab" tabindex="0">
|
||||
<div class="tab-change" role="tab" tabindex="0">
|
||||
<a>Comment</a>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
<li data-id="tab-quotation-{{ book.id }}" data-category="tab-option-{{ book.id }}">
|
||||
<label for="quote-{{ book.id }}">
|
||||
<div onclick="tabChange(event)" role="tab" tabindex="0">
|
||||
<div class="tab-change" role="tab" tabindex="0">
|
||||
<a>Quote</a>
|
||||
</div>
|
||||
</label>
|
||||
|
@ -39,5 +39,5 @@
|
|||
|
||||
<div>
|
||||
<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>
|
||||
|
|
|
@ -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 %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
|
@ -9,7 +9,8 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<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' %}
|
||||
|
||||
{% if type == 'review' %}
|
||||
<fieldset>
|
||||
|
@ -27,16 +28,13 @@
|
|||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
{% include 'snippets/content_warning_field.html' %}
|
||||
|
||||
{% if type == 'quote' %}
|
||||
{% if type == 'quotation' %}
|
||||
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
|
||||
{% else %}
|
||||
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% if type == 'quote' %}
|
||||
{% if type == 'quotation' %}
|
||||
<div class="control">
|
||||
<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>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% if book %}
|
||||
{% include 'snippets/book_cover.html' with book=book %}
|
||||
{% if ratings %}
|
||||
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
|
||||
{% endif %}
|
||||
|
||||
<h3 class="title is-6"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3>
|
||||
{% if book.authors %}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% with status.id|uuid as uuid %}
|
||||
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
||||
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-small" type="submit">
|
||||
<span class="icon icon-heart">
|
||||
<span class="icon icon-heart" title="Like status">
|
||||
<span class="is-sr-only">Like status</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
||||
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-success is-small" type="submit">
|
||||
<span class="icon icon-heart">
|
||||
<span class="icon icon-heart" title="Un-like status">
|
||||
<span class="is-sr-only">Un-like status</span>
|
||||
</span>
|
||||
</button>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Finish "{{ book.title }}"</p>
|
||||
<label class="delete" for="finish-reading-{{ uuid }}" aria-label="close" role="button"></label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with label="close" controls_text="finish-reading" controls_uid=uuid class="delete" %}
|
||||
</header>
|
||||
{% active_read_through book user as readthrough %}
|
||||
<form name="finish-reading" action="/finish-reading/{{ book.id }}" method="post">
|
||||
|
@ -29,15 +29,15 @@
|
|||
<footer class="modal-card-foot">
|
||||
<div class="columns">
|
||||
<div class="column field">
|
||||
<label for="post-status">
|
||||
<input type="checkbox" name="post-status" class="checkbox" checked>
|
||||
<label for="post_status-{{ uuid }}">
|
||||
<input type="checkbox" name="post-status" class="checkbox" id="post_status-{{ uuid }}" checked>
|
||||
Post to feed
|
||||
</label>
|
||||
{% include 'snippets/privacy_select.html' %}
|
||||
</div>
|
||||
<div class="column">
|
||||
<button type="submit" class="button is-success">Save</button>
|
||||
<label for="finish-reading-{{ uuid }}" class="button" role="button">Cancel</button>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="finish-reading" controls_uid=uuid %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -7,7 +7,7 @@ Follow request already sent.
|
|||
|
||||
{% else %}
|
||||
|
||||
<form action="/follow/" method="POST" onsubmit="interact(event)" class="follow-{{ user.id }} {% if request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
||||
<form action="/follow/" method="POST" class="interaction follow-{{ user.id }} {% if request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ user.username }}">
|
||||
{% if user.manually_approves_followers %}
|
||||
|
@ -16,7 +16,7 @@ Follow request already sent.
|
|||
<button class="button is-small is-link" type="submit">Follow</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<form action="/unfollow/" method="POST" onsubmit="interact(event)" class="follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
||||
<form action="/unfollow/" method="POST" class="interaction follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ user.username }}">
|
||||
<button class="button is-small is-danger is-light" type="submit">Unfollow</button>
|
||||
|
|
34
bookwyrm/templates/snippets/goal_form.html
Normal file
34
bookwyrm/templates/snippets/goal_form.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<form method="post" name="goal" action="{{ request.user.local_path }}/goal/{{ year }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="year" value="{% if goal %}{{ goal.year }}{% else %}{{ year }}{% endif %}">
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<label class="label" for="id_goal">Reading goal:</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input type="number" class="input" name="goal" id="id_goal" value="{% if goal %}{{ goal.goal }}{% else %}12{% endif %}">
|
||||
</div>
|
||||
<p class="button is-static" aria-hidden="true">books</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<label class="label"><p class="mb-2">Goal privacy:</p>
|
||||
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label for="post_status" class="label">
|
||||
<input type="checkbox" name="post-status" id="post_status" class="checkbox" checked>
|
||||
Post to feed
|
||||
</label>
|
||||
|
||||
<p>
|
||||
<button type="submit" class="button is-link">Set goal</button>
|
||||
{% if goal %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="hide-edit-goal" %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</form>
|
10
bookwyrm/templates/snippets/goal_progress.html
Normal file
10
bookwyrm/templates/snippets/goal_progress.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<p>
|
||||
{% if goal.progress_percent >= 100 %}
|
||||
Success!
|
||||
{% elif goal.progress_percent %}
|
||||
{{ goal.progress_percent }}% complete!
|
||||
{% endif %}
|
||||
{% if goal.user == request.user %}You've{% else %}{{ goal.user.display_name }} has{% endif %} read {% if request.path != goal.local_path %}<a href="{{ goal.local_path }}">{% endif %}{{ goal.book_count }} of {{ goal.goal }} books{% if request.path != goal.local_path %}</a>{% endif %}.
|
||||
</p>
|
||||
<progress class="progress is-large" value="{{ goal.book_count }}" max="{{ goal.goal }}" aria-hidden="true">{{ goal.progress_percent }}%</progress>
|
||||
|
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,17 +1,17 @@
|
|||
{% if item.privacy == 'public' %}
|
||||
<span class="icon icon-globe">
|
||||
<span class="icon icon-globe" title="Public post">
|
||||
<span class="is-sr-only">Public post</span>
|
||||
</span>
|
||||
{% elif item.privacy == 'unlisted' %}
|
||||
<span class="icon icon-unlock">
|
||||
<span class="icon icon-unlock" title="Unlisted post">
|
||||
<span class="is-sr-only">Unlisted post</span>
|
||||
</span>
|
||||
{% elif item.privacy == 'followers' %}
|
||||
<span class="icon icon-lock">
|
||||
<span class="icon icon-lock" title="Followers-only post">
|
||||
<span class="is-sr-only">Followers-only post</span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="icon icon-envelope">
|
||||
<span class="icon icon-envelope" title="Private post">
|
||||
<span class="is-sr-only">Private post</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
|
|
@ -26,16 +26,8 @@
|
|||
{% endif %}
|
||||
</dl>
|
||||
<div class="field is-grouped">
|
||||
<label class="button is-small" for="edit-readthrough-{{ readthrough.id }}" role="button" tabindex="0">
|
||||
<span class="icon icon-pencil">
|
||||
<span class="is-sr-only">Edit read-through dates</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="button is-small" for="delete-readthrough-{{ readthrough.id }}" role="button" tabindex="0">
|
||||
<span class="icon icon-x">
|
||||
<span class="is-sr-only">Delete this read-through</span>
|
||||
</span>
|
||||
</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with small=True text="Edit read dates" icon="pencil" controls_text="edit-readthrough" controls_uid=readthrough.id %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with small=True text="Delete these read dates" icon="x" controls_text="delete-readthrough" controls_uid=readthrough.id %}
|
||||
</div>
|
||||
{% if show_progress %}
|
||||
Progress Updates:
|
||||
|
@ -78,7 +70,7 @@
|
|||
{% include 'snippets/readthrough_form.html' with readthrough=readthrough %}
|
||||
<div class="field is-grouped">
|
||||
<button class="button is-primary" type="submit">Save</button>
|
||||
<label class="button" for="show-readthrough-{{ readthrough.id }}" role="button" tabindex="0">Cancel</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="show-readthrough" controls_uid=readthrough.id %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -101,7 +93,7 @@
|
|||
<button class="button is-danger is-light" type="submit">
|
||||
Delete
|
||||
</button>
|
||||
<label for="delete-readthrough-{{ readthrough.id }}" class="button" role="button" tabindex="0">Cancel</button>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="delete-readthrough" controls_uid=readthrough.id %}
|
||||
</form>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% with status.id|uuid as uuid %}
|
||||
<form class="is-flex-grow-1" name="reply" action="/reply" method="post" onsubmit="return reply(event)">
|
||||
<div class="columns">
|
||||
<form class="is-flex-grow-1" name="reply" action="/post/reply" method="post">
|
||||
<div class="columns is-align-items-flex-end">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="reply_parent" value="{{ status.id }}">
|
||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||
<div class="column">
|
||||
|
||||
{% include 'snippets/content_warning_field.html' with parent_status=status %}
|
||||
<label for="id_content_{{ status.id }}-{{ uuid }}" class="is-sr-only">Reply</label>
|
||||
<div class="field">
|
||||
|
@ -25,4 +23,3 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endwith %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="dropdown">
|
||||
<div class="dropdown-trigger">
|
||||
<label for="shelf-select-dropdown-{{ book.id }}-toggle" role="button" aria-expanded="false" onclick="toggleMenu(this)" tabindex="0" aria-haspopup="true" aria-controls="shelf-select-{{ book.id }}">
|
||||
<label for="shelf-select-dropdown-{{ book.id }}-toggle" role="button" aria-expanded="false" class="pulldown-menu" tabindex="0" aria-haspopup="true" aria-controls="shelf-select-{{ book.id }}">
|
||||
<div class="button">
|
||||
<span>Change shelf</span>
|
||||
<span class="icon icon-arrow-down" aria-hidden="true"></span>
|
||||
|
|
|
@ -13,15 +13,9 @@
|
|||
<span>Read</span> <span class="icon icon-check"></span>
|
||||
</button>
|
||||
{% elif active_shelf.shelf.identifier == 'reading' %}
|
||||
<label class="button is-small" for="finish-reading-{{ uuid }}" role="button" tabindex="0">
|
||||
I'm done!
|
||||
</label>
|
||||
{% include 'snippets/finish_reading_modal.html' with book=active_shelf.book %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with small=True text="I'm done!" controls_text="finish-reading" controls_uid=uuid %}
|
||||
{% elif active_shelf.shelf.identifier == 'to-read' %}
|
||||
<label class="button is-small" for="start-reading-{{ uuid }}" role="button" tabindex="0">
|
||||
Start reading
|
||||
</label>
|
||||
{% include 'snippets/start_reading_modal.html' with book=active_shelf.book %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with small=True text="Start reading" controls_text="start-reading" controls_uid=uuid %}
|
||||
{% else %}
|
||||
<form name="shelve" action="/shelve/" method="post">
|
||||
{% csrf_token %}
|
||||
|
@ -33,7 +27,7 @@
|
|||
|
||||
<div class="dropdown">
|
||||
<div class="dropdown-trigger">
|
||||
<label for="shelf-select-dropdown-{{ uuid }}-toggle" role="button" aria-expanded="false" onclick="toggleMenu(this)" tabindex="0" aria-haspopup="true" aria-controls="shelf-select-{{ uuid }}">
|
||||
<label for="shelf-select-dropdown-{{ uuid }}-toggle" role="button" aria-expanded="false" class="pulldown-menu" tabindex="0" aria-haspopup="true" aria-controls="shelf-select-{{ uuid }}">
|
||||
<div class="button is-small">
|
||||
<span class="icon icon-arrow-down"><span class="is-sr-only">More shelves</span></span>
|
||||
</div>
|
||||
|
@ -46,10 +40,7 @@
|
|||
<li role="menuitem">
|
||||
{% if active_shelf.shelf.identifier != 'reading' and shelf.identifier == 'reading' %}
|
||||
<div class="dropdown-item pt-0 pb-0">
|
||||
<label class="button is-small" for="start-reading-{{ uuid }}" role="button" tabindex="0">
|
||||
Start reading
|
||||
</label>
|
||||
{% include 'snippets/start_reading_modal.html' with book=active_shelf.book %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with small=True text="Start reading" controls_text="start-reading" controls_uid=uuid %}
|
||||
</div>
|
||||
{% else %}
|
||||
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/shelve/" method="post">
|
||||
|
@ -68,5 +59,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'snippets/start_reading_modal.html' with book=active_shelf.book %}
|
||||
{% include 'snippets/finish_reading_modal.html' with book=active_shelf.book %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Start "{{ book.title }}"</p>
|
||||
<label class="delete" for="start-reading-{{ uuid }}" aria-label="close" role="button" tabindex="0"></label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with label="close" controls_text="start-reading" controls_uid=uuid class="delete" %}
|
||||
</header>
|
||||
<form name="start-reading" action="/start-reading/{{ book.id }}" method="post">
|
||||
<section class="modal-card-body">
|
||||
|
@ -20,15 +20,15 @@
|
|||
<footer class="modal-card-foot">
|
||||
<div class="columns">
|
||||
<div class="column field">
|
||||
<label for="post-status">
|
||||
<input type="checkbox" name="post-status" class="checkbox" checked>
|
||||
<label for="post_status_start-{{ uuid }}">
|
||||
<input type="checkbox" name="post-status" class="checkbox" id="post_status_start-{{ uuid }}" checked>
|
||||
Post to feed
|
||||
</label>
|
||||
{% include 'snippets/privacy_select.html' %}
|
||||
</div>
|
||||
<div class="column">
|
||||
<button class="button is-success" type="submit">Save</button>
|
||||
<label for="start-reading-{{ uuid }}" class="button" role="button" tabindex="0">Cancel</button>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="start-reading" controls_uid=uuid %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -21,27 +21,21 @@
|
|||
<div class="card-footer has-background-white-bis">
|
||||
<div class="card-footer-item">
|
||||
{% if request.user.is_authenticated %}
|
||||
<label class="button is-small" for="show-comment-{{ status.id }}">
|
||||
<div role="button" tabindex="0">
|
||||
<span class="icon icon-comment">
|
||||
<span class="is-sr-only">Reply</span>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show-comment" controls_uid=status.id text="Reply" icon='comment' small=True %}
|
||||
{% include 'snippets/boost_button.html' with status=status %}
|
||||
{% include 'snippets/fav_button.html' with status=status %}
|
||||
|
||||
{% else %}
|
||||
<a href="/login">
|
||||
<span class="icon icon-comment">
|
||||
<span class="icon icon-comment" title="Reply">
|
||||
<span class="is-sr-only">Reply</span>
|
||||
</span>
|
||||
|
||||
<span class="icon icon-boost">
|
||||
<span class="icon icon-boost" title="Boost status">
|
||||
<span class="is-sr-only">Boost status</span>
|
||||
</span>
|
||||
|
||||
<span class="icon icon-heart">
|
||||
<span class="icon icon-heart" title="Like status">
|
||||
<span class="is-sr-only">Like status</span>
|
||||
</span>
|
||||
</a>
|
||||
|
@ -57,17 +51,14 @@
|
|||
</div>
|
||||
{% if status.user == request.user %}
|
||||
<div class="card-footer-item">
|
||||
<label class="button is-small" for="more-info-{{ status.id }}">
|
||||
<div class="icon icon-dots-three">
|
||||
<span class="is-sr-only">More options</span>
|
||||
</div>
|
||||
</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="more-info" controls_uid=status.id text="More options" icon="dots-three" small=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<input class="toggle-control" type="checkbox" name="show-comment-{{ status.id }}" id="show-comment-{{ status.id }}">
|
||||
{% with status.id|uuid as uuid %}
|
||||
<input class="toggle-control" type="checkbox" name="show-comment-{{ status.id }}" id="show-comment-{{ status.id }}" data-hover-target="id_content_{{ status.id }}-{{ uuid }}">
|
||||
<div class="toggle-content hidden">
|
||||
<div class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
|
@ -75,6 +66,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if status.user == request.user %}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<p>{{ status.content_warning }}</p>
|
||||
<input class="toggle-control" type="radio" name="toggle-status-cw-{{ status.id }}" id="hide-status-cw-{{ status.id }}" checked>
|
||||
<div class="toggle-content hidden">
|
||||
<label class="button is-small" for="show-status-cw-{{ status.id }}" tabindex="0" role="button">Show More</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Show More" small=True controls_text="show-status-cw" controls_uid=status.id %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
|||
{% endif %}
|
||||
<div{% if status.content_warning %} class="toggle-content hidden"{% endif %}>
|
||||
{% if status.content_warning %}
|
||||
<label class="button is-small" for="hide-status-cw-{{ status.id }}" tabindex="0" role="button">Show Less</label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Show Less" small=True controls_text="hide-status-cw" controls_uid=status.id %}
|
||||
{% endif %}
|
||||
|
||||
{% if status.quote %}
|
||||
|
|
9
bookwyrm/templates/snippets/toggle/toggle_button.html
Normal file
9
bookwyrm/templates/snippets/toggle/toggle_button.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<label class="{% if class %}{{ class }}{% else %}button{% endif %}{% if small %} is-small{% endif %}" for="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}" tabindex="0" role="button"{% if label %} aria-label="{{ label }}"{% endif %}>
|
||||
{% if icon %}
|
||||
<span class="icon icon-{{ icon }}" title="{{ text }}">
|
||||
<span class="is-sr-only">{{ text }}</span>
|
||||
</span>
|
||||
{% else %}
|
||||
{{ text }}
|
||||
{% endif %}
|
||||
</label>
|
|
@ -7,13 +7,13 @@
|
|||
<div>
|
||||
<input type="radio" name="show-hide-{{ uuid }}" id="show-{{ uuid }}" class="toggle-control" checked>
|
||||
<blockquote class="content toggle-content hidden"><span dir="auto">{{ trimmed | to_markdown | safe }}</span>
|
||||
<label class="button is-small" for="hide-{{ uuid }}"><div role="button" tabindex="0">show more</div></label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="show more" controls_text="hide" controls_uid=uuid class="has-text-link is-clickable" %}
|
||||
</blockquote>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" name="show-hide-{{ uuid }}" id="hide-{{ uuid }}" class="toggle-control">
|
||||
<blockquote class="content toggle-content hidden"><span dir="auto">{{ full | to_markdown | safe }}</span>
|
||||
<label class="button is-small" for="show-{{ uuid }}"><div role="button" tabindex="0">show less</div></label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="show less" controls_text="show" controls_uid=uuid class="has-text-link is-clickable" %}
|
||||
</blockquote>
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
</div>
|
||||
{% if is_self %}
|
||||
<div class="column is-narrow">
|
||||
<a href="/user-edit/">
|
||||
<span class="icon icon-pencil">
|
||||
<a href="/edit-profile">
|
||||
<span class="icon icon-pencil" title="Edit profile">
|
||||
<span class="is-sr-only">Edit profile</span>
|
||||
</span>
|
||||
</a>
|
||||
|
@ -40,12 +40,23 @@
|
|||
<small><a href="{{ user.local_path }}/shelves">See all {{ shelf_count }} shelves</a></small>
|
||||
</div>
|
||||
|
||||
{% if goal %}
|
||||
<div class="block">
|
||||
<h2 class="title">{% now 'Y' %} Reading Goal</h2>
|
||||
{% include 'snippets/goal_progress.html' with goal=goal %}
|
||||
</div>
|
||||
{% elif user == request.user %}
|
||||
<div class="block">
|
||||
<h2 class="title is-4"><a href="{{ user.local_path }}/goal/{% now 'Y' %}">Set a reading goal for {% now 'Y' %}</a></h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<div class="block">
|
||||
<h2 class="title">User Activity</h2>
|
||||
</div>
|
||||
{% for activity in activities %}
|
||||
<div class="block">
|
||||
<div class="block" id="feed">
|
||||
{% include 'snippets/status.html' with status=activity %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -55,25 +66,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||
{% 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>
|
||||
{% include 'snippets/pagination.html' with page=activities path=user.local_path anchor="#feed" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,7 +7,7 @@ from django import template
|
|||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.outgoing import to_markdown
|
||||
from bookwyrm.views.status import to_markdown
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
''' testing import '''
|
||||
from collections import namedtuple
|
||||
import csv
|
||||
import pathlib
|
||||
from unittest.mock import patch
|
||||
|
||||
|
@ -30,6 +31,12 @@ class GoodreadsImport(TestCase):
|
|||
search_url='https://%s/search?q=' % DOMAIN,
|
||||
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):
|
||||
|
@ -97,8 +104,140 @@ class GoodreadsImport(TestCase):
|
|||
'bookwyrm.models.import_job.ImportItem.get_book_from_isbn'
|
||||
) as resolve:
|
||||
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)
|
||||
|
||||
import_item = models.ImportItem.objects.get(job=import_job, index=0)
|
||||
self.assertEqual(import_item.book.id, book.id)
|
||||
|
||||
|
||||
def test_handle_imported_book(self):
|
||||
''' goodreads import added a book, this adds related connections '''
|
||||
shelf = self.user.shelf_set.filter(identifier='read').first()
|
||||
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
|
||||
from unittest.mock import patch
|
||||
|
||||
from dateutil.parser import parse
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
@ -213,3 +212,53 @@ class TemplateTags(TestCase):
|
|||
r'[A-Z][a-z]{2} \d?\d \d{4}',
|
||||
bookwyrm_tags.time_since(years_ago)
|
||||
))
|
||||
|
||||
|
||||
def test_get_markdown(self):
|
||||
''' mardown format data '''
|
||||
result = bookwyrm_tags.get_markdown('_hi_')
|
||||
self.assertEqual(result, '<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
|
||||
)
|
114
bookwyrm/tests/views/test_goal.py
Normal file
114
bookwyrm/tests/views/test_goal.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
''' 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, views
|
||||
|
||||
|
||||
class GoalViews(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.rat = models.User.objects.create_user(
|
||||
'rat@local.com', 'rat@rat.com', 'ratword',
|
||||
local=True, localname='rat',
|
||||
remote_id='https://example.com/users/rat',
|
||||
)
|
||||
self.book = models.Edition.objects.create(
|
||||
title='Example Edition',
|
||||
remote_id='https://example.com/book/1',
|
||||
)
|
||||
self.anonymous_user = AnonymousUser
|
||||
self.anonymous_user.is_authenticated = False
|
||||
|
||||
|
||||
def test_goal_page_no_goal(self):
|
||||
''' view a reading goal page for another's unset goal '''
|
||||
view = views.Goal.as_view()
|
||||
request = self.factory.get('')
|
||||
request.user = self.rat
|
||||
|
||||
result = view(request, self.local_user.localname, 2020)
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
def test_goal_page_no_goal_self(self):
|
||||
''' view a reading goal page for your own unset goal '''
|
||||
view = views.Goal.as_view()
|
||||
request = self.factory.get('')
|
||||
request.user = self.local_user
|
||||
|
||||
result = view(request, self.local_user.localname, 2020)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
|
||||
|
||||
def test_goal_page_anonymous(self):
|
||||
''' can't view it without login '''
|
||||
view = views.Goal.as_view()
|
||||
request = self.factory.get('')
|
||||
request.user = self.anonymous_user
|
||||
|
||||
result = view(request, self.local_user.localname, 2020)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
def test_goal_page_public(self):
|
||||
''' view a user's public goal '''
|
||||
models.AnnualGoal.objects.create(
|
||||
user=self.local_user,
|
||||
year=2020,
|
||||
goal=128937123,
|
||||
privacy='public')
|
||||
view = views.Goal.as_view()
|
||||
request = self.factory.get('')
|
||||
request.user = self.rat
|
||||
|
||||
result = view(request, self.local_user.localname, 2020)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
|
||||
def test_goal_page_private(self):
|
||||
''' view a user's private goal '''
|
||||
models.AnnualGoal.objects.create(
|
||||
user=self.local_user,
|
||||
year=2020,
|
||||
goal=15,
|
||||
privacy='followers')
|
||||
view = views.Goal.as_view()
|
||||
request = self.factory.get('')
|
||||
request.user = self.rat
|
||||
|
||||
result = view(request, self.local_user.localname, 2020)
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
|
||||
def test_create_goal(self):
|
||||
''' create a new goal '''
|
||||
view = views.Goal.as_view()
|
||||
request = self.factory.post('', {
|
||||
'user': self.local_user.id,
|
||||
'goal': 10,
|
||||
'year': 2020,
|
||||
'privacy': 'unlisted',
|
||||
'post-status': True
|
||||
})
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
view(request, self.local_user.localname, 2020)
|
||||
|
||||
goal = models.AnnualGoal.objects.get()
|
||||
self.assertEqual(goal.user, self.local_user)
|
||||
self.assertEqual(goal.goal, 10)
|
||||
self.assertEqual(goal.year, 2020)
|
||||
self.assertEqual(goal.privacy, 'unlisted')
|
||||
|
||||
status = models.GeneratedNote.objects.get()
|
||||
self.assertEqual(status.user, self.local_user)
|
||||
self.assertEqual(status.privacy, 'unlisted')
|
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')
|
180
bookwyrm/urls.py
180
bookwyrm/urls.py
|
@ -3,8 +3,7 @@ from django.conf.urls.static import static
|
|||
from django.contrib import admin
|
||||
from django.urls import path, re_path
|
||||
|
||||
from bookwyrm import incoming, outgoing, views, settings, wellknown
|
||||
from bookwyrm import view_actions as actions
|
||||
from bookwyrm import incoming, settings, views, wellknown
|
||||
from bookwyrm.utils import regex
|
||||
|
||||
user_path = r'^user/(?P<username>%s)' % regex.username
|
||||
|
@ -31,7 +30,7 @@ urlpatterns = [
|
|||
# federation endpoints
|
||||
re_path(r'^inbox/?$', incoming.shared_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
|
||||
re_path(r'^.well-known/webfinger/?$', wellknown.webfinger),
|
||||
|
@ -39,110 +38,101 @@ urlpatterns = [
|
|||
re_path(r'^nodeinfo/2\.0/?$', wellknown.nodeinfo),
|
||||
re_path(r'^api/v1/instance/?$', wellknown.instance_info),
|
||||
re_path(r'^api/v1/instance/peers/?$', wellknown.peers),
|
||||
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),
|
||||
# TODO: robots.txt
|
||||
|
||||
# ui views
|
||||
re_path(r'^login/?$', views.login_page),
|
||||
re_path(r'^about/?$', views.about_page),
|
||||
re_path(r'^password-reset/?$', views.password_reset_request),
|
||||
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$', views.password_reset),
|
||||
re_path(r'^invite/?$', views.manage_invites),
|
||||
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.invite_page),
|
||||
# authentication
|
||||
re_path(r'^login/?$', views.Login.as_view()),
|
||||
re_path(r'^register/?$', views.Register.as_view()),
|
||||
re_path(r'^logout/?$', views.Logout.as_view()),
|
||||
re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()),
|
||||
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$',
|
||||
views.PasswordReset.as_view()),
|
||||
re_path(r'^change-password/?$', views.ChangePassword),
|
||||
|
||||
path('', views.home),
|
||||
re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab),
|
||||
re_path(r'^discover/?$', views.discover_page),
|
||||
re_path(r'^notifications/?$', views.notifications_page),
|
||||
re_path(r'^direct-messages/?$', views.direct_messages_page),
|
||||
re_path(r'^import/?$', views.import_page),
|
||||
re_path(r'^import-status/(\d+)/?$', views.import_status),
|
||||
re_path(r'^user-edit/?$', views.edit_profile_page),
|
||||
# invites
|
||||
re_path(r'^invite/?$', views.ManageInvites.as_view()),
|
||||
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.Invite.as_view()),
|
||||
|
||||
# landing pages
|
||||
re_path(r'^about/?$', views.About.as_view()),
|
||||
path('', views.Home.as_view()),
|
||||
re_path(r'^(?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
|
||||
re_path(r'%s/?$' % user_path, views.user_page),
|
||||
re_path(r'%s\.json$' % local_user_path, views.user_page),
|
||||
re_path(r'%s/?$' % local_user_path, views.user_page),
|
||||
re_path(r'%s/shelves/?$' % local_user_path, views.user_shelves_page),
|
||||
re_path(r'%s/followers(.json)?/?$' % local_user_path, views.followers_page),
|
||||
re_path(r'%s/following(.json)?/?$' % local_user_path, views.following_page),
|
||||
re_path(r'%s/?$' % user_path, views.User.as_view()),
|
||||
re_path(r'%s\.json$' % user_path, views.User.as_view()),
|
||||
re_path(r'%s/shelves/?$' % user_path, views.user_shelves_page),
|
||||
re_path(r'%s/followers(.json)?/?$' % user_path, views.Followers.as_view()),
|
||||
re_path(r'%s/following(.json)?/?$' % user_path, views.Following.as_view()),
|
||||
re_path(r'^edit-profile/?$', views.EditUser.as_view()),
|
||||
|
||||
# reading goals
|
||||
re_path(r'%s/goal/(?P<year>\d{4})/?$' % user_path, views.Goal.as_view()),
|
||||
|
||||
# statuses
|
||||
re_path(r'%s(.json)?/?$' % status_path, views.status_page),
|
||||
re_path(r'%s/activity/?$' % status_path, views.status_page),
|
||||
re_path(r'%s/replies(.json)?/?$' % status_path, views.replies_page),
|
||||
re_path(r'%s(.json)?/?$' % status_path, views.Status.as_view()),
|
||||
re_path(r'%s/activity/?$' % status_path, views.Status.as_view()),
|
||||
re_path(r'%s/replies(.json)?/?$' % status_path, views.Replies.as_view()),
|
||||
re_path(r'^post/(?P<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
|
||||
re_path(r'%s(.json)?/?$' % book_path, views.book_page),
|
||||
re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
|
||||
re_path(r'^author/(?P<author_id>[\w\-]+)/edit/?$', views.edit_author_page),
|
||||
re_path(r'%s/editions(.json)?/?$' % book_path, views.editions_page),
|
||||
re_path(r'%s(.json)?/?$' % book_path, views.Book.as_view()),
|
||||
re_path(r'%s/edit/?$' % book_path, views.EditBook.as_view()),
|
||||
re_path(r'%s/editions(.json)?/?$' % book_path, views.Editions.as_view()),
|
||||
re_path(r'^upload-cover/(?P<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),
|
||||
re_path(r'^tag/(?P<tag_id>.+)\.json/?$', views.tag_page),
|
||||
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),
|
||||
# author
|
||||
re_path(r'^author/(?P<author_id>\d+)(.json)?/?$', views.Author.as_view()),
|
||||
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)?/?$' % \
|
||||
user_path, views.shelf_page),
|
||||
user_path, views.Shelf.as_view()),
|
||||
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),
|
||||
re_path(r'^delete-progressupdate/?$', views.delete_progressupdate),
|
||||
|
||||
# internal action endpoints
|
||||
re_path(r'^logout/?$', actions.user_logout),
|
||||
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'^delete-progressupdate/?$', actions.delete_progressupdate),
|
||||
|
||||
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),
|
||||
re_path(r'^start-reading/(?P<book_id>\d+)/?$', views.start_reading),
|
||||
re_path(r'^finish-reading/(?P<book_id>\d+)/?$', views.finish_reading),
|
||||
|
||||
# following
|
||||
re_path(r'^follow/?$', views.follow),
|
||||
re_path(r'^unfollow/?$', views.unfollow),
|
||||
re_path(r'^accept-follow-request/?$', views.accept_follow_request),
|
||||
re_path(r'^delete-follow-request/?$', views.delete_follow_request),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
@ -1,892 +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()
|
||||
|
||||
# create a progress update if we have a page
|
||||
readthrough.create_update()
|
||||
|
||||
# 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()
|
||||
|
||||
# record the progress update individually
|
||||
# use default now for date field
|
||||
readthrough.create_update()
|
||||
|
||||
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 delete_progressupdate(request):
|
||||
''' remove a progress update '''
|
||||
update = get_object_or_404(models.ProgressUpdate, id=request.POST.get('id'))
|
||||
|
||||
# don't let people edit other people's data
|
||||
if request.user != update.user:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
update.delete()
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
@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
|
||||
|
||||
progress = request.POST.get('progress')
|
||||
if progress:
|
||||
try:
|
||||
progress = int(progress)
|
||||
readthrough.progress = progress
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
progress_mode = request.POST.get('progress_mode')
|
||||
if progress_mode:
|
||||
try:
|
||||
progress_mode = models.ProgressMode(progress_mode)
|
||||
readthrough.progress_mode = progress_mode
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not readthrough.start_date and not readthrough.finish_date:
|
||||
return None
|
||||
|
||||
return readthrough
|
|
@ -1,842 +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')
|
||||
|
||||
for readthrough in readthroughs:
|
||||
readthrough.progress_updates = \
|
||||
readthrough.progressupdate_set.all().order_by('-updated_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,
|
||||
'show_progress': ('showprogress' in request.GET),
|
||||
'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)
|
26
bookwyrm/views/__init__.py
Normal file
26
bookwyrm/views/__init__.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
''' 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 .goal import Goal
|
||||
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)
|
233
bookwyrm/views/books.py
Normal file
233
bookwyrm/views/books.py
Normal file
|
@ -0,0 +1,233 @@
|
|||
''' 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')
|
||||
|
||||
for readthrough in readthroughs:
|
||||
readthrough.progress_updates = \
|
||||
readthrough.progressupdate_set.all().order_by('-updated_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,
|
||||
'show_progress': ('showprogress' in request.GET),
|
||||
'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)
|
79
bookwyrm/views/goal.py
Normal file
79
bookwyrm/views/goal.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
''' non-interactive pages '''
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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.broadcast import broadcast
|
||||
from bookwyrm.status import create_generated_note
|
||||
from .helpers import get_user_from_username, object_visible_to_user
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class Goal(View):
|
||||
''' track books for the year '''
|
||||
def get(self, request, username, year):
|
||||
''' reading goal page '''
|
||||
user = get_user_from_username(username)
|
||||
year = int(year)
|
||||
goal = models.AnnualGoal.objects.filter(
|
||||
year=year, user=user
|
||||
).first()
|
||||
if not goal and user != request.user:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if goal and not object_visible_to_user(request.user, goal):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
data = {
|
||||
'title': '%s\'s %d Reading' % (user.display_name, year),
|
||||
'goal_form': forms.GoalForm(instance=goal),
|
||||
'goal': goal,
|
||||
'user': user,
|
||||
'year': year,
|
||||
}
|
||||
return TemplateResponse(request, 'goal.html', data)
|
||||
|
||||
|
||||
def post(self, request, username, year):
|
||||
''' update or create an annual goal '''
|
||||
user = get_user_from_username(username)
|
||||
if user != request.user:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
year = int(year)
|
||||
goal = models.AnnualGoal.objects.filter(
|
||||
year=year, user=request.user
|
||||
).first()
|
||||
form = forms.GoalForm(request.POST, instance=goal)
|
||||
if not form.is_valid():
|
||||
data = {
|
||||
'title': '%s\'s %d Reading' % (goal.user.display_name, year),
|
||||
'goal_form': form,
|
||||
'goal': goal,
|
||||
'year': year,
|
||||
}
|
||||
return TemplateResponse(request, 'goal.html', data)
|
||||
goal = form.save()
|
||||
|
||||
if request.POST.get('post-status'):
|
||||
# create status, if appropraite
|
||||
status = create_generated_note(
|
||||
request.user,
|
||||
'set a goal to read %d books in %d' % (goal.goal, goal.year),
|
||||
privacy=goal.privacy
|
||||
)
|
||||
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', '/'))
|
174
bookwyrm/views/helpers.py
Normal file
174
bookwyrm/views/helpers.py
Normal file
|
@ -0,0 +1,174 @@
|
|||
''' 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 object_visible_to_user(viewer, obj):
|
||||
''' is a user authorized to view an object? '''
|
||||
if viewer == obj.user or obj.privacy in ['public', 'unlisted']:
|
||||
return True
|
||||
if obj.privacy == 'followers' and \
|
||||
obj.user.followers.filter(id=viewer.id).first():
|
||||
return True
|
||||
if isinstance(obj, models.Status):
|
||||
if obj.privacy == 'direct' and \
|
||||
obj.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
|
129
bookwyrm/views/landing.py
Normal file
129
bookwyrm/views/landing.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
''' 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 import timezone
|
||||
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)
|
||||
|
||||
goal = models.AnnualGoal.objects.filter(
|
||||
user=request.user, year=timezone.now().year
|
||||
).first()
|
||||
data = {
|
||||
'title': 'Updates Feed',
|
||||
'user': request.user,
|
||||
'suggested_books': suggested_books,
|
||||
'activities': paginated.page(page),
|
||||
'tab': tab,
|
||||
'goal': goal,
|
||||
'goal_form': forms.GoalForm(),
|
||||
}
|
||||
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
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue