Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2020-12-27 14:45:53 -08:00
commit 2d21a31c13
34 changed files with 767 additions and 367 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,7 +1,7 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: bookwrym
patreon: bookwyrm
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel

View file

@ -3,7 +3,7 @@ from dataclasses import dataclass, fields, MISSING
from json import JSONEncoder
from django.apps import apps
from django.db import transaction
from django.db import IntegrityError, transaction
from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.tasks import app
@ -92,7 +92,10 @@ class ActivityObject:
with transaction.atomic():
# we can't set many to many and reverse fields on an unsaved object
instance.save()
try:
instance.save()
except IntegrityError as e:
raise ActivitySerializerError(e)
# add many to many fields, which have to be set post-save
for field in instance.many_to_many_fields:
@ -108,15 +111,10 @@ class ActivityObject:
continue
model_field = getattr(model, model_field_name)
try:
# this is for one to many
related_model = model_field.field.model
related_field_name = model_field.field.name
except AttributeError:
# it's a one to one or foreign key
related_model = model_field.related.related_model
related_field_name = model_field.related.related_name
values = [values]
# creating a Work, model_field is 'editions'
# creating a User, model field is 'key_pair'
related_model = model_field.field.model
related_field_name = model_field.field.name
for item in values:
set_related_field.delay(
@ -139,8 +137,8 @@ class ActivityObject:
@app.task
@transaction.atomic
def set_related_field(
model_name, origin_model_name,
related_field_name, related_remote_id, data):
model_name, origin_model_name, related_field_name,
related_remote_id, data):
''' load reverse related fields (editions, attachments) without blocking '''
model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True)
origin_model = apps.get_model(
@ -150,23 +148,36 @@ def set_related_field(
with transaction.atomic():
if isinstance(data, str):
item = resolve_remote_id(model, data, save=False)
else:
# look for a match based on all the available data
item = model.find_existing(data)
if not item:
# create a new model instance
item = model.activity_serializer(**data)
item = item.to_model(model, save=False)
existing = model.find_existing_by_remote_id(data)
if existing:
data = existing.to_activity()
else:
data = get_data(data)
activity = model.activity_serializer(**data)
# this must exist because it's the object that triggered this function
instance = origin_model.find_existing_by_remote_id(related_remote_id)
if not instance:
raise ValueError(
'Invalid related remote id: %s' % related_remote_id)
# edition.parent_work = instance, for example
setattr(item, related_field_name, instance)
item.save()
# set the origin's remote id on the activity so it will be there when
# the model instance is created
# edition.parentWork = instance, for example
model_field = getattr(model, related_field_name)
if hasattr(model_field, 'activitypub_field'):
setattr(
activity,
getattr(model_field, 'activitypub_field'),
instance.remote_id
)
item = activity.to_model(model)
# if the related field isn't serialized (attachments on Status), then
# we have to set it post-creation
if not hasattr(model_field, 'activitypub_field'):
setattr(item, related_field_name, instance)
item.save()
def resolve_remote_id(model, remote_id, refresh=False, save=True):

View file

@ -63,5 +63,7 @@ class Author(ActivityObject):
aliases: List[str] = field(default_factory=lambda: [])
bio: str = ''
openlibraryKey: str = ''
librarythingKey: str = ''
goodreadsKey: str = ''
wikipediaLink: str = ''
type: str = 'Person'

View file

@ -168,7 +168,7 @@ class AbstractConnector(AbstractMinimalConnector):
''' every work needs at least one edition '''
@abstractmethod
def get_work_from_edition_date(self, data):
def get_work_from_edition_data(self, data):
''' every edition needs a work '''
@abstractmethod
@ -228,6 +228,7 @@ class SearchResult:
key: str
author: str
year: str
connector: object
confidence: int = 1
def __repr__(self):

View file

@ -13,4 +13,5 @@ class Connector(AbstractMinimalConnector):
return data
def format_search_result(self, search_result):
search_result['connector'] = self
return SearchResult(**search_result)

View file

@ -85,7 +85,7 @@ class Connector(AbstractConnector):
return pick_default_edition(data['entries'])
def get_work_from_edition_date(self, data):
def get_work_from_edition_data(self, data):
try:
key = data['works'][0]['key']
except (IndexError, KeyError):
@ -123,6 +123,7 @@ class Connector(AbstractConnector):
title=search_result.get('title'),
key=key,
author=', '.join(author),
connector=self,
year=search_result.get('first_publish_year'),
)

View file

@ -51,28 +51,23 @@ class Connector(AbstractConnector):
author=search_result.author_text,
year=search_result.published_date.year if \
search_result.published_date else None,
connector=self,
confidence=search_result.rank,
)
def get_remote_id_from_data(self, data):
pass
def is_work_data(self, data):
pass
def get_edition_from_work_data(self, data):
pass
def get_work_from_edition_date(self, data):
def get_work_from_edition_data(self, data):
pass
def get_authors_from_data(self, data):
return None
def get_cover_from_data(self, data):
return None
def parse_search_data(self, data):
''' it's already in the right format, don't even worry about it '''
return data

View file

@ -30,6 +30,7 @@ class CustomForm(ModelForm):
visible.field.widget.attrs['rows'] = None
visible.field.widget.attrs['class'] = css_classes[input_type]
# pylint: disable=missing-class-docstring
class LoginForm(CustomForm):
class Meta:
@ -121,14 +122,13 @@ class EditionForm(CustomForm):
model = models.Edition
exclude = [
'remote_id',
'origin_id',
'created_date',
'updated_date',
'last_sync_date',
'authors',# TODO
'parent_work',
'shelves',
'misc_identifiers',
'subjects',# TODO
'subject_places',# TODO
@ -136,6 +136,16 @@ class EditionForm(CustomForm):
'connector',
]
class AuthorForm(CustomForm):
class Meta:
model = models.Author
exclude = [
'remote_id',
'origin_id',
'created_date',
'updated_date',
]
class ImportForm(forms.Form):
csv_file = forms.FileField()

View file

@ -0,0 +1,61 @@
# Generated by Django 3.0.7 on 2020-12-21 20:14
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', '0028_remove_book_author_text'),
]
operations = [
migrations.RemoveField(
model_name='author',
name='last_sync_date',
),
migrations.RemoveField(
model_name='author',
name='sync',
),
migrations.RemoveField(
model_name='book',
name='last_sync_date',
),
migrations.RemoveField(
model_name='book',
name='sync',
),
migrations.RemoveField(
model_name='book',
name='sync_cover',
),
migrations.AddField(
model_name='author',
name='goodreads_key',
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='author',
name='last_edited_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='author',
name='librarything_key',
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='book',
name='last_edited_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='author',
name='origin_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View file

@ -2,7 +2,7 @@
import inspect
import sys
from .book import Book, Work, Edition
from .book import Book, Work, Edition, BookDataModel
from .author import Author
from .connector import Connector

View file

@ -1,21 +1,15 @@
''' database schema for info about authors '''
from django.db import models
from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .base_model import ActivitypubMixin, BookWyrmModel
from .book import BookDataModel
from . import fields
class Author(ActivitypubMixin, BookWyrmModel):
class Author(BookDataModel):
''' basic biographic info '''
origin_id = models.CharField(max_length=255, null=True)
openlibrary_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True)
sync = models.BooleanField(default=True)
last_sync_date = models.DateTimeField(default=timezone.now)
wikipedia_link = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True)
# idk probably other keys would be useful here?
@ -27,15 +21,6 @@ class Author(ActivitypubMixin, BookWyrmModel):
)
bio = fields.HtmlField(null=True, blank=True)
def save(self, *args, **kwargs):
''' handle remote vs origin ids '''
if self.id:
self.remote_id = self.get_remote_id()
else:
self.origin_id = self.remote_id
self.remote_id = None
return super().save(*args, **kwargs)
def get_remote_id(self):
''' editions and works both use "book" instead of model_name '''
return 'https://%s/author/%s' % (DOMAIN, self.id)

View file

@ -2,7 +2,6 @@
import re
from django.db import models
from django.utils import timezone
from model_utils.managers import InheritanceManager
from bookwyrm import activitypub
@ -12,10 +11,9 @@ from .base_model import BookWyrmModel
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from . import fields
class Book(ActivitypubMixin, BookWyrmModel):
''' a generic book, which can mean either an edition or a work '''
class BookDataModel(ActivitypubMixin, BookWyrmModel):
''' fields shared between editable book data (books, works, authors) '''
origin_id = models.CharField(max_length=255, null=True, blank=True)
# these identifiers apply to both works and editions
openlibrary_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True)
librarything_key = fields.CharField(
@ -23,15 +21,28 @@ class Book(ActivitypubMixin, BookWyrmModel):
goodreads_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True)
# info about where the data comes from and where/if to sync
sync = models.BooleanField(default=True)
sync_cover = models.BooleanField(default=True)
last_sync_date = models.DateTimeField(default=timezone.now)
last_edited_by = models.ForeignKey(
'User', on_delete=models.PROTECT, null=True)
class Meta:
''' can't initialize this model, that wouldn't make sense '''
abstract = True
def save(self, *args, **kwargs):
''' ensure that the remote_id is within this instance '''
if self.id:
self.remote_id = self.get_remote_id()
else:
self.origin_id = self.remote_id
self.remote_id = None
return super().save(*args, **kwargs)
class Book(BookDataModel):
''' a generic book, which can mean either an edition or a work '''
connector = models.ForeignKey(
'Connector', on_delete=models.PROTECT, null=True)
# TODO: edit history
# book/work metadata
title = fields.CharField(max_length=255)
sort_title = fields.CharField(max_length=255, blank=True, null=True)
@ -48,9 +59,7 @@ class Book(ActivitypubMixin, BookWyrmModel):
subject_places = fields.ArrayField(
models.CharField(max_length=255), blank=True, null=True, default=list
)
# TODO: include an annotation about the type of authorship (ie, translator)
authors = fields.ManyToManyField('Author')
# preformatted authorship string for search and easier display
cover = fields.ImageField(
upload_to='covers/', blank=True, null=True, alt_field='alt_text')
first_published_date = fields.DateTimeField(blank=True, null=True)
@ -86,12 +95,6 @@ class Book(ActivitypubMixin, BookWyrmModel):
''' can't be abstract for query reasons, but you shouldn't USE it '''
if not isinstance(self, Edition) and not isinstance(self, Work):
raise ValueError('Books should be added as Editions or Works')
if self.id:
self.remote_id = self.get_remote_id()
else:
self.origin_id = self.remote_id
self.remote_id = None
return super().save(*args, **kwargs)
def get_remote_id(self):

View file

@ -76,7 +76,7 @@ class ImportItem(models.Model):
)
if search_result:
# raises ConnectorException
return books_manager.get_or_create_book(search_result.key)
return search_result.connector.get_or_create_book(search_result.key)
return None
@ -91,7 +91,7 @@ class ImportItem(models.Model):
)
if search_result:
# raises ConnectorException
return books_manager.get_or_create_book(search_result.key)
return search_result.connector.get_or_create_book(search_result.key)
return None

View file

@ -54,7 +54,7 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
def to_reject_activity(self):
''' generate an Accept for this follow request '''
''' generate a Reject for this follow request '''
return activitypub.Reject(
id=self.get_remote_id(status='rejects'),
actor=self.user_object.remote_id,

View file

@ -375,9 +375,9 @@ def handle_unboost(user, status):
broadcast(user, activity)
def handle_update_book(user, book):
def handle_update_book_data(user, item):
''' broadcast the news about our book '''
broadcast(user, book.to_update_activity(user))
broadcast(user, item.to_update_activity(user))
def handle_update_user(user):

View file

@ -2,13 +2,31 @@
{% load bookwyrm_tags %}
{% block content %}
<div class="block">
<h1 class="title">{{ author.name }}</h1>
<div class="columns">
<div class="column">
<h1 class="title">{{ author.name }}</h1>
</div>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
<div class="column is-narrow">
<a href="/author/{{ author.id }}/edit">
<span class="icon icon-pencil">
<span class="is-sr-only">Edit Author</span>
</span>
</a>
</div>
{% endif %}
</div>
</div>
<div class="block">
{% if author.bio %}
<p>
{{ author.bio }}
{{ author.bio | to_markdown | safe }}
</p>
{% endif %}
{% if author.wikipedia_link %}
<p><a href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">Wikipedia</a></p>
{% endif %}
</div>
<div class="block">

View file

@ -0,0 +1,89 @@
{% extends 'layout.html' %}
{% load humanize %}
{% block content %}
<div class="block">
<div class="level">
<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>
{% if form.non_field_errors %}
<div class="block">
<p class="notification is-danger">{{ form.non_field_errors }}</p>
</div>
{% endif %}
<form class="block" name="edit-author" action="/edit-author/{{ author.id }}" method="post">
{% csrf_token %}
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<div class="columns">
<div class="column">
<h2 class="title is-4">Metadata</h2>
<p><label class="label" for="id_name">Name:</label> {{ form.name }}</p>
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_bio">Bio:</label> {{ form.bio }}</p>
{% for error in form.bio.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_wikipedia_link">Wikipedia link:</label> {{ form.wikipedia_link }}</p>
{% for error in form.wikipedia_link.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_born">Birth date:</label> {{ form.born }}</p>
{% for error in form.born.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_died">Death date:</label> {{ form.died }}</p>
{% for error in form.died.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="column">
<h2 class="title is-4">Author Identifiers</h2>
<p><label class="label" for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }}</p>
{% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }}</p>
{% for error in form.librarything_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }}</p>
{% for error in form.goodreads_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
<div class="block">
<button class="button is-primary" type="submit">Save</button>
<a class="button" href="/author/{{ author.id }}">Cancel</a>
</div>
</form>
{% endblock %}

View file

@ -17,63 +17,51 @@
<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>
{% if login_form.non_field_errors %}
{% if form.non_field_errors %}
<div class="block">
<p class="notification is-danger">{{ login_form.non_field_errors }}</p>
<p class="notification is-danger">{{ form.non_field_errors }}</p>
</div>
{% endif %}
<form class="block" name="edit-book" action="/edit-book/{{ book.id }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<h2 class="title is-4">Data sync
<p class="subtitle is-6">If sync is enabled, any changes will be over-written</p>
</h2>
<div class="columns">
<div class="column is-narrow">
<label class="checkbox" for="id_sync"><input class="checkbox" type="checkbox" name="sync" id="id_sync"> Sync</label>
</div>
<div class="column is-narrow">
<label class="checkbox" for="id_sync_cover"><input class="checkbox" type="checkbox" name="sync_cover" id="id_sync_cover"> Sync cover</label>
</div>
</div>
</div>
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<div class="columns">
<div class="column">
<h2 class="title is-4">Metadata</h2>
<p class="fields is-grouped"><label class="label"for="id_title">Title:</label> {{ form.title }} </p>
<p class="fields is-grouped"><label class="label" for="id_title">Title:</label> {{ form.title }} </p>
{% for error in form.title.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_sort_title">Sort title:</label> {{ form.sort_title }} </p>
<p class="fields is-grouped"><label class="label" for="id_sort_title">Sort title:</label> {{ form.sort_title }} </p>
{% for error in form.sort_title.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_subtitle">Subtitle:</label> {{ form.subtitle }} </p>
<p class="fields is-grouped"><label class="label" for="id_subtitle">Subtitle:</label> {{ form.subtitle }} </p>
{% for error in form.subtitle.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_description">Description:</label> {{ form.description }} </p>
<p class="fields is-grouped"><label class="label" for="id_description">Description:</label> {{ form.description }} </p>
{% for error in form.description.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_series">Series:</label> {{ form.series }} </p>
<p class="fields is-grouped"><label class="label" for="id_series">Series:</label> {{ form.series }} </p>
{% for error in form.series.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_series_number">Series number:</label> {{ form.series_number }} </p>
<p class="fields is-grouped"><label class="label" for="id_series_number">Series number:</label> {{ form.series_number }} </p>
{% for error in form.series_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_first_published_date">First published date:</label> {{ form.first_published_date }} </p>
<p class="fields is-grouped"><label class="label" for="id_first_published_date">First published date:</label> {{ form.first_published_date }} </p>
{% for error in form.first_published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_published_date">Published date:</label> {{ form.published_date }} </p>
<p class="fields is-grouped"><label class="label" for="id_published_date">Published date:</label> {{ form.published_date }} </p>
{% for error in form.published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -97,7 +85,7 @@
<div class="block">
<h2 class="title is-4">Physical Properties</h2>
<p class="fields is-grouped"><label class="label"for="id_physical_format">Format:</label> {{ form.physical_format }} </p>
<p class="fields is-grouped"><label class="label" for="id_physical_format">Format:</label> {{ form.physical_format }} </p>
{% for error in form.physical_format.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -105,7 +93,7 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_pages">Pages:</label> {{ form.pages }} </p>
<p class="fields is-grouped"><label class="label" for="id_pages">Pages:</label> {{ form.pages }} </p>
{% for error in form.pages.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -113,23 +101,23 @@
<div class="block">
<h2 class="title is-4">Book Identifiers</h2>
<p class="fields is-grouped"><label class="label"for="id_isbn_13">ISBN 13:</label> {{ form.isbn_13 }} </p>
<p class="fields is-grouped"><label class="label" for="id_isbn_13">ISBN 13:</label> {{ form.isbn_13 }} </p>
{% for error in form.isbn_13.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_isbn_10">ISBN 10:</label> {{ form.isbn_10 }} </p>
<p class="fields is-grouped"><label class="label" for="id_isbn_10">ISBN 10:</label> {{ form.isbn_10 }} </p>
{% for error in form.isbn_10.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p>
<p class="fields is-grouped"><label class="label" for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p>
{% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }} </p>
<p class="fields is-grouped"><label class="label" for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }} </p>
{% for error in form.librarything_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label"for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }} </p>
<p class="fields is-grouped"><label class="label" for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }} </p>
{% for error in form.goodreads_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}

View file

@ -44,18 +44,28 @@
<div>
<input class="toggle-control" type="radio" name="recent-books" id="book-{{ book.id }}" {% if shelf_counter == 1 and forloop.first %}checked{% endif %}>
<div class="toggle-content hidden" role="tabpanel" id="book-{{ book.id }}-panel">
<div class="box">
<div class="block">
{% include 'snippets/book_titleby.html' with book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
<div class="card">
<div class="card-header">
<p class="card-header-title">
<span>{% include 'snippets/book_titleby.html' with book=book %}</span>
</>
<div class="card-header-icon is-hidden-tablet">
<label class="delete" for="no-book" aria-label="close" role="button"></label>
</div>
</div>
<div class="card-content">
{% include 'snippets/shelve_button.html' with book=book %}
{% include 'snippets/create_status.html' with book=book %}
</div>
{% include 'snippets/create_status.html' with book=book %}
</div>
</div>
</div>
{% endfor %}
{% endwith %}
{% endfor %}
<div>
<input class="toggle-control" type="radio" name="recent-books" id="no-book">
</div>
{% endif %}
</div>

View file

@ -1,7 +1,7 @@
{% load bookwyrm_tags %}
<div class="cover-container is-{{ size }}">
{% if book.cover %}
<img class="book-cover" src="/images/{{ book.cover }}" alt="{{ book.alt_text }}">
<img class="book-cover" src="/images/{{ book.cover }}" alt="{{ book.alt_text }}" title="{{ book.alt_text }}">
{% else %}
<div class="no-cover book-cover">
<img class="book-cover" src="/static/images/no_cover.jpg" alt="No cover">

View file

@ -1,9 +1,5 @@
<span>
<a href="/book/{{ book.id }}">{{ book.title }}</a>
</span>
<a href="/book/{{ book.id }}">{{ book.title }}</a>
{% if book.authors %}
<span>
by {% include 'snippets/authors.html' with book=book %}
</span>
by {% include 'snippets/authors.html' with book=book %}
{% endif %}

View file

@ -43,7 +43,7 @@
<div class="column is-narrow">
<figure class="image is-128x128">
<a href="/images/{{ attachment.image }}" target="_blank" aria-label="open image in new window">
<img src="/images/{{ attachment.image }}" alt="{{ attachment.caption }}">
<img src="/images/{{ attachment.image }}"{% if attachment.caption %} alt="{{ attachment.caption }}" title="{{ attachment.caption }}"{% endif %}>
</a>
</figure>
</div>

View file

@ -0,0 +1,52 @@
{
"id": "https://example.com/users/rat/generatednote/2567/activity",
"type": "Create",
"actor": "https://example.com/users/rat",
"object": {
"id": "https://example.com/users/rat/generatednote/2567",
"type": "GeneratedNote",
"url": null,
"inReplyTo": null,
"published": "2020-12-16T01:45:19.662734+00:00",
"attributedTo": "https://example.com/users/rat",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://example.com/users/rat/followers"
],
"content": "wants to read",
"replies": {
"id": "https://example.com/users/rat/generatednote/2567/replies",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://example.com/users/rat/generatednote/2567/replies?page=true",
"last": "https://example.com/users/rat/generatednote/2567/replies?page=true",
"name": "",
"@context": "https://www.w3.org/ns/activitystreams"
},
"tag": [
{
"href": "https://bookwyrm.social/book/37292",
"name": "Female Husbands",
"type": "Book"
}
],
"attachment": [],
"sensitive": false,
"@context": "https://www.w3.org/ns/activitystreams"
},
"to": [
"https://example.com/users/rat/followers"
],
"cc": [
"https://www.w3.org/ns/activitystreams#Public"
],
"signature": {
"creator": "https://example.com/users/rat#main-key",
"created": "2020-12-16T01:45:19.662734+00:00",
"signatureValue": "R+W8nN1CQAlREjSUeaQwJXZrXTOOLvpHQi9n/3vd8QKq+l6HJEpu7eAht9fjpk8YOKEgV3OUQ7w3E42wM4t+sFiaPoQjY6Xy9IOvx/2LcOZjSOtTkiZ1XnnVb3DSbl8BOBH02+cPvoR6k4LIPHm2IHYZ1UL02WdDWaicHEwl7bw=",
"type": "RsaSignature2017"
},
"@context": "https://www.w3.org/ns/activitystreams"
}

View file

@ -1,9 +1,15 @@
''' testing models '''
import datetime
import json
import pathlib
from unittest.mock import patch
from django.utils import timezone
from django.test import TestCase
import responses
from bookwyrm import models
from bookwyrm import books_manager, models
from bookwyrm.connectors.abstract_connector import SearchResult
class ImportJob(TestCase):
@ -54,11 +60,11 @@ class ImportJob(TestCase):
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True)
job = models.ImportJob.objects.create(user=user)
models.ImportItem.objects.create(
self.item_1 = models.ImportItem.objects.create(
job=job, index=1, data=currently_reading_data)
models.ImportItem.objects.create(
self.item_2 = models.ImportItem.objects.create(
job=job, index=2, data=read_data)
models.ImportItem.objects.create(
self.item_3 = models.ImportItem.objects.create(
job=job, index=3, data=unknown_read_data)
@ -72,8 +78,7 @@ class ImportJob(TestCase):
def test_shelf(self):
''' converts to the local shelf typology '''
expected = 'reading'
item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.shelf, expected)
self.assertEqual(self.item_1.shelf, expected)
def test_date_added(self):
@ -91,21 +96,75 @@ class ImportJob(TestCase):
def test_currently_reading_reads(self):
''' infer currently reading dates where available '''
expected = [models.ReadThrough(
start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc))]
start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc)
)]
actual = models.ImportItem.objects.get(index=1)
self.assertEqual(actual.reads[0].start_date, expected[0].start_date)
self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date)
def test_read_reads(self):
actual = models.ImportItem.objects.get(index=2)
self.assertEqual(actual.reads[0].start_date, datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc))
self.assertEqual(actual.reads[0].finish_date, datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc))
''' infer read dates where available '''
actual = self.item_2
self.assertEqual(
actual.reads[0].start_date,
datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc))
self.assertEqual(
actual.reads[0].finish_date,
datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc))
def test_unread_reads(self):
''' handle books with no read dates '''
expected = []
actual = models.ImportItem.objects.get(index=3)
self.assertEqual(actual.reads, expected)
@responses.activate
def test_get_book_from_isbn(self):
''' search and load books by isbn (9780356506999) '''
connector_info = models.Connector.objects.create(
identifier='openlibrary.org',
name='OpenLibrary',
connector_file='openlibrary',
base_url='https://openlibrary.org',
books_url='https://openlibrary.org',
covers_url='https://covers.openlibrary.org',
search_url='https://openlibrary.org/search?q=',
priority=3,
)
connector = books_manager.load_connector(connector_info)
result = SearchResult(
title='Test Result',
key='https://openlibrary.org/works/OL1234W',
author='An Author',
year='1980',
connector=connector,
)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ol_edition.json')
bookdata = json.loads(datafile.read_bytes())
responses.add(
responses.GET,
'https://openlibrary.org/works/OL1234W',
json=bookdata,
status=200)
responses.add(
responses.GET,
'https://openlibrary.org//works/OL15832982W',
json=bookdata,
status=200)
responses.add(
responses.GET,
'https://openlibrary.org//authors/OL382982A.json',
json={'name': 'test author'},
status=200)
with patch('bookwyrm.books_manager.first_search_result') as search:
search.return_value = result
book = self.item_1.get_book_from_isbn()
self.assertEqual(book.title, 'Sabriel')

View file

@ -1 +0,0 @@
from . import *

View file

@ -1,80 +0,0 @@
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, outgoing
from bookwyrm.settings import DOMAIN
class Following(TestCase):
def setUp(self):
with patch('bookwyrm.models.user.set_remote_server'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True,
remote_id='http://local.com/users/mouse',
)
def test_handle_follow(self):
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
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):
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):
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):
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
)

View file

@ -1,61 +0,0 @@
''' testing user lookup '''
import json
import pathlib
from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import models, outgoing
from bookwyrm.settings import DOMAIN
class TestOutgoingRemoteWebfinger(TestCase):
''' overwrites standard model feilds to work with activitypub '''
def setUp(self):
''' get user data ready '''
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json'
)
self.userdata = json.loads(datafile.read_bytes())
del self.userdata['icon']
def test_existing_user(self):
''' simple database lookup by username '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True)
result = outgoing.handle_remote_webfinger('@mouse@%s' % DOMAIN)
self.assertEqual(result, user)
result = outgoing.handle_remote_webfinger('mouse@%s' % DOMAIN)
self.assertEqual(result, user)
@responses.activate
def test_load_user(self):
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')

View file

@ -1,69 +0,0 @@
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, outgoing
class Shelving(TestCase):
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True,
remote_id='http://local.com/users/mouse',
)
work = models.Work.objects.create(
title='Example 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.user
)
def test_handle_shelve(self):
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_shelve(self.user, self.book, self.shelf)
# make sure the book is on the shelf
self.assertEqual(self.shelf.books.get(), self.book)
def test_handle_shelve_to_read(self):
shelf = models.Shelf.objects.get(identifier='to-read')
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_shelve(self.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):
shelf = models.Shelf.objects.get(identifier='reading')
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_shelve(self.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):
shelf = models.Shelf.objects.get(identifier='read')
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_shelve(self.user, self.book, shelf)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
def test_handle_unshelve(self):
self.shelf.books.add(self.book)
self.shelf.save()
self.assertEqual(self.shelf.books.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_unshelve(self.user, self.book, self.shelf)
self.assertEqual(self.shelf.books.count(), 0)

View file

@ -8,6 +8,7 @@ from django.http import HttpResponseBadRequest, HttpResponseNotAllowed, \
HttpResponseNotFound
from django.test import TestCase
from django.test.client import RequestFactory
import responses
from bookwyrm import models, incoming
@ -421,6 +422,25 @@ class Incoming(TestCase):
self.assertEqual(notification.related_status, self.status)
@responses.activate
def test_handle_discarded_boost(self):
''' test a boost of a mastodon status that will be discarded '''
activity = {
'type': 'Announce',
'id': 'http://www.faraway.com/boost/12',
'actor': self.remote_user.remote_id,
'object': self.status.to_activity(),
}
responses.add(
responses.GET,
'http://www.faraway.com/boost/12',
json={'id': 'http://www.faraway.com/boost/12'},
status=200)
incoming.handle_boost(activity)
self.assertEqual(models.Boost.objects.count(), 0)
def test_handle_unboost(self):
''' undo a boost '''
activity = {

View file

@ -0,0 +1,193 @@
''' sending out activities '''
import json
import pathlib
from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import models, outgoing
from bookwyrm.settings import DOMAIN
class Outgoing(TestCase):
''' sends out activities '''
def setUp(self):
''' we'll need some data '''
with patch('bookwyrm.models.user.set_remote_server'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True,
remote_id='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_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@%s' % DOMAIN)
self.assertEqual(result, self.local_user)
result = outgoing.handle_remote_webfinger('mouse@%s' % DOMAIN)
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)

View file

@ -2,11 +2,13 @@
from unittest.mock import patch
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 bookwyrm import view_actions as actions, models
from bookwyrm import forms, models, view_actions as actions
from bookwyrm.settings import DOMAIN
@ -19,6 +21,13 @@ class ViewActions(TestCase):
'mouse', 'mouse@mouse.com', 'mouseword', local=True)
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',
@ -238,6 +247,43 @@ class ViewActions(TestCase):
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_switch_edition(self):
''' updates user's relationships to a book '''
work = models.Work.objects.create(title='test work')
@ -262,3 +308,46 @@ class ViewActions(TestCase):
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')

View file

@ -76,6 +76,7 @@ urlpatterns = [
# 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'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_page),
@ -104,6 +105,7 @@ urlpatterns = [
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),

View file

@ -159,23 +159,21 @@ def password_change(request):
request.user.set_password(new_password)
request.user.save()
login(request, request.user)
return redirect('/user-edit')
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)
form = forms.EditUserForm(
request.POST, request.FILES, instance=request.user)
if not form.is_valid():
data = {
'form': form,
'user': request.user,
}
data = {'form': form, 'user': request.user}
return TemplateResponse(request, 'edit_user.html', data)
request.user.name = form.data['name']
request.user.email = form.data['email']
user = form.save(commit=False)
if 'avatar' in form.files:
# crop and resize avatar upload
image = Image.open(form.files['avatar'])
@ -201,17 +199,10 @@ def edit_profile(request):
# set the name to a hash
extension = form.files['avatar'].name.split('.')[-1]
filename = '%s.%s' % (uuid4(), extension)
request.user.avatar.save(
filename,
ContentFile(output.getvalue())
)
user.avatar.save(filename, ContentFile(output.getvalue()))
user.save()
request.user.summary = form.data['summary']
request.user.manually_approves_followers = \
form.cleaned_data['manually_approves_followers']
request.user.save()
outgoing.handle_update_user(request.user)
outgoing.handle_update_user(user)
return redirect('/user/%s' % request.user.localname)
@ -244,7 +235,7 @@ def edit_book(request, book_id):
return TemplateResponse(request, 'edit_book.html', data)
book = form.save()
outgoing.handle_update_book(request.user, book)
outgoing.handle_update_book_data(request.user, book)
return redirect('/book/%s' % book.id)
@ -289,10 +280,9 @@ def upload_cover(request, book_id):
return redirect('/book/%d' % book.id)
book.cover = form.files['cover']
book.sync_cover = False
book.save()
outgoing.handle_update_book(request.user, book)
outgoing.handle_update_book_data(request.user, book)
return redirect('/book/%s' % book.id)
@ -311,10 +301,31 @@ def add_description(request, book_id):
book.description = description
book.save()
outgoing.handle_update_book(request.user, book)
outgoing.handle_update_book_data(request.user, book)
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()
outgoing.handle_update_book_data(request.user, author)
return redirect('/author/%s' % author.id)
@login_required
@require_POST
def create_shelf(request):

View file

@ -657,6 +657,20 @@ def edit_book_page(request, book_id):
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 '''