Merge pull request #416 from mouse-reeve/book-data-model

Remove sync fields and share fields between book and author
This commit is contained in:
Mouse Reeve 2020-12-22 10:25:12 -08:00 committed by GitHub
commit c34d90051f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 322 additions and 76 deletions

View file

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

View file

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

@ -1,21 +1,15 @@
''' database schema for info about authors ''' ''' database schema for info about authors '''
from django.db import models from django.db import models
from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from .base_model import ActivitypubMixin, BookWyrmModel from .book import BookDataModel
from . import fields from . import fields
class Author(ActivitypubMixin, BookWyrmModel): class Author(BookDataModel):
''' basic biographic info ''' ''' 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( wikipedia_link = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True) max_length=255, blank=True, null=True, deduplication_field=True)
# idk probably other keys would be useful here? # idk probably other keys would be useful here?
@ -27,15 +21,6 @@ class Author(ActivitypubMixin, BookWyrmModel):
) )
bio = fields.HtmlField(null=True, blank=True) 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): def get_remote_id(self):
''' editions and works both use "book" instead of model_name ''' ''' editions and works both use "book" instead of model_name '''
return 'https://%s/author/%s' % (DOMAIN, self.id) return 'https://%s/author/%s' % (DOMAIN, self.id)

View file

@ -2,7 +2,6 @@
import re import re
from django.db import models from django.db import models
from django.utils import timezone
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from bookwyrm import activitypub from bookwyrm import activitypub
@ -12,10 +11,9 @@ from .base_model import BookWyrmModel
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from . import fields from . import fields
class Book(ActivitypubMixin, BookWyrmModel): class BookDataModel(ActivitypubMixin, BookWyrmModel):
''' a generic book, which can mean either an edition or a work ''' ''' fields shared between editable book data (books, works, authors) '''
origin_id = models.CharField(max_length=255, null=True, blank=True) origin_id = models.CharField(max_length=255, null=True, blank=True)
# these identifiers apply to both works and editions
openlibrary_key = fields.CharField( openlibrary_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True) max_length=255, blank=True, null=True, deduplication_field=True)
librarything_key = fields.CharField( librarything_key = fields.CharField(
@ -23,15 +21,28 @@ class Book(ActivitypubMixin, BookWyrmModel):
goodreads_key = fields.CharField( goodreads_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True) max_length=255, blank=True, null=True, deduplication_field=True)
# info about where the data comes from and where/if to sync last_edited_by = models.ForeignKey(
sync = models.BooleanField(default=True) 'User', on_delete=models.PROTECT, null=True)
sync_cover = models.BooleanField(default=True)
last_sync_date = models.DateTimeField(default=timezone.now) 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 = models.ForeignKey(
'Connector', on_delete=models.PROTECT, null=True) 'Connector', on_delete=models.PROTECT, null=True)
# TODO: edit history
# book/work metadata # book/work metadata
title = fields.CharField(max_length=255) title = fields.CharField(max_length=255)
sort_title = fields.CharField(max_length=255, blank=True, null=True) sort_title = fields.CharField(max_length=255, blank=True, null=True)
@ -86,12 +97,6 @@ class Book(ActivitypubMixin, BookWyrmModel):
''' can't be abstract for query reasons, but you shouldn't USE it ''' ''' can't be abstract for query reasons, but you shouldn't USE it '''
if not isinstance(self, Edition) and not isinstance(self, Work): if not isinstance(self, Edition) and not isinstance(self, Work):
raise ValueError('Books should be added as Editions or Works') 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) return super().save(*args, **kwargs)
def get_remote_id(self): def get_remote_id(self):

View file

@ -375,9 +375,9 @@ def handle_unboost(user, status):
broadcast(user, activity) broadcast(user, activity)
def handle_update_book(user, book): def handle_update_book_data(user, item):
''' broadcast the news about our book ''' ''' 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): def handle_update_user(user):

View file

@ -2,13 +2,31 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% block content %} {% block content %}
<div class="block"> <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 %} {% if author.bio %}
<p> <p>
{{ author.bio }} {{ author.bio | to_markdown | safe }}
</p> </p>
{% endif %} {% endif %}
{% if author.wikipedia_link %}
<p><a href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">Wikipedia</a></p>
{% endif %}
</div> </div>
<div class="block"> <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> <div>
<p>Added: {{ book.created_date | naturaltime }}</p> <p>Added: {{ book.created_date | naturaltime }}</p>
<p>Updated: {{ book.updated_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>
</div> </div>
{% if login_form.non_field_errors %} {% if form.non_field_errors %}
<div class="block"> <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> </div>
{% endif %} {% endif %}
<form class="block" name="edit-book" action="/edit-book/{{ book.id }}" method="post" enctype="multipart/form-data"> <form class="block" name="edit-book" action="/edit-book/{{ book.id }}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="block"> <input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<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>
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<h2 class="title is-4">Metadata</h2> <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 %} {% for error in form.title.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.sort_title.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.subtitle.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.description.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.series.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.series_number.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.first_published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
@ -97,7 +85,7 @@
<div class="block"> <div class="block">
<h2 class="title is-4">Physical Properties</h2> <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 %} {% for error in form.physical_format.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
@ -105,7 +93,7 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.pages.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
@ -113,23 +101,23 @@
<div class="block"> <div class="block">
<h2 class="title is-4">Book Identifiers</h2> <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 %} {% for error in form.isbn_13.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.isbn_10.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.librarything_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.goodreads_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}

View file

@ -2,6 +2,8 @@
from unittest.mock import patch from unittest.mock import patch
from django.core.exceptions import PermissionDenied 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.http.response import Http404
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -19,6 +21,13 @@ class ViewActions(TestCase):
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse', 'mouse@mouse.com', 'mouseword', local=True)
self.local_user.remote_id = 'https://example.com/user/mouse' self.local_user.remote_id = 'https://example.com/user/mouse'
self.local_user.save() 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'): with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword', 'rat', 'rat@rat.com', 'ratword',
@ -248,7 +257,7 @@ class ViewActions(TestCase):
}) })
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.view_actions.login'): with patch('bookwyrm.view_actions.login'):
resp = actions.password_change(request) actions.password_change(request)
self.assertNotEqual(self.local_user.password, password_hash) self.assertNotEqual(self.local_user.password, password_hash)
def test_password_change_mismatch(self): def test_password_change_mismatch(self):
@ -259,7 +268,7 @@ class ViewActions(TestCase):
'confirm-password': 'hihi' 'confirm-password': 'hihi'
}) })
request.user = self.local_user request.user = self.local_user
resp = actions.password_change(request) actions.password_change(request)
self.assertEqual(self.local_user.password, password_hash) self.assertEqual(self.local_user.password, password_hash)
@ -299,3 +308,46 @@ class ViewActions(TestCase):
self.assertEqual(models.ShelfBook.objects.get().book, edition2) self.assertEqual(models.ShelfBook.objects.get().book, edition2)
self.assertEqual(models.ReadThrough.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 # books
re_path(r'%s(.json)?/?$' % book_path, views.book_page), re_path(r'%s(.json)?/?$' % book_path, views.book_page),
re_path(r'%s/edit/?$' % book_path, views.edit_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/editions(.json)?/?$' % book_path, views.editions_page),
re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_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'^edit-book/(?P<book_id>\d+)/?$', actions.edit_book),
re_path(r'^upload-cover/(?P<book_id>\d+)/?$', actions.upload_cover), 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'^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'^switch-edition/?$', actions.switch_edition),
re_path(r'^edit-readthrough/?$', actions.edit_readthrough), re_path(r'^edit-readthrough/?$', actions.edit_readthrough),

View file

@ -235,7 +235,7 @@ def edit_book(request, book_id):
return TemplateResponse(request, 'edit_book.html', data) return TemplateResponse(request, 'edit_book.html', data)
book = form.save() book = form.save()
outgoing.handle_update_book(request.user, book) outgoing.handle_update_book_data(request.user, book)
return redirect('/book/%s' % book.id) return redirect('/book/%s' % book.id)
@ -280,10 +280,9 @@ def upload_cover(request, book_id):
return redirect('/book/%d' % book.id) return redirect('/book/%d' % book.id)
book.cover = form.files['cover'] book.cover = form.files['cover']
book.sync_cover = False
book.save() book.save()
outgoing.handle_update_book(request.user, book) outgoing.handle_update_book_data(request.user, book)
return redirect('/book/%s' % book.id) return redirect('/book/%s' % book.id)
@ -302,10 +301,31 @@ def add_description(request, book_id):
book.description = description book.description = description
book.save() book.save()
outgoing.handle_update_book(request.user, book) outgoing.handle_update_book_data(request.user, book)
return redirect('/book/%s' % book.id) 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 @login_required
@require_POST @require_POST
def create_shelf(request): def create_shelf(request):

View file

@ -657,6 +657,20 @@ def edit_book_page(request, book_id):
return TemplateResponse(request, 'edit_book.html', data) 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 @require_GET
def editions_page(request, book_id): def editions_page(request, book_id):
''' list of editions of a book ''' ''' list of editions of a book '''