Edit books

Fixes #109
This commit is contained in:
Mouse Reeve 2020-03-28 15:06:16 -07:00
parent 16fec1b6d5
commit 1a33290267
11 changed files with 281 additions and 44 deletions

View file

@ -75,5 +75,34 @@ class TagForm(ModelForm):
labels = {'name': 'Add a tag'} labels = {'name': 'Add a tag'}
class CoverForm(ModelForm):
class Meta:
model = models.Book
fields = ['cover']
help_texts = {f: None for f in fields}
class BookForm(ModelForm):
class Meta:
model = models.Book
exclude = [
'created_date',
'updated_date',
'last_sync_date',
'authors',
'parent_work',
'shelves',
'misc_identifiers',
'subjects',
'subject_places',
'source_url',
'connector',
]
class ImportForm(forms.Form): class ImportForm(forms.Form):
csv_file = forms.FileField() csv_file = forms.FileField()

View file

@ -0,0 +1,114 @@
# Generated by Django 3.0.3 on 2020-03-28 22:03
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0022_auto_20200328_2001'),
]
operations = [
migrations.AddField(
model_name='book',
name='sync_cover',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='author',
name='born',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='author',
name='died',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='author',
name='fedireads_key',
field=models.CharField(default=uuid.uuid4, max_length=255, unique=True),
),
migrations.AlterField(
model_name='author',
name='first_name',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='author',
name='last_name',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='author',
name='openlibrary_key',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='book',
name='first_published_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='book',
name='goodreads_key',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='book',
name='language',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='book',
name='librarything_key',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='book',
name='openlibrary_key',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='book',
name='published_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='book',
name='sort_title',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='book',
name='subtitle',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='edition',
name='isbn',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='edition',
name='oclc_number',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='edition',
name='pages',
field=models.IntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='edition',
name='physical_format',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='work',
name='lccn',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View file

@ -47,15 +47,16 @@ class Connector(FedireadsModel):
class Book(FedireadsModel): class Book(FedireadsModel):
''' a generic book, which can mean either an edition or a work ''' ''' a generic book, which can mean either an edition or a work '''
# these identifiers apply to both works and editions # these identifiers apply to both works and editions
openlibrary_key = models.CharField(max_length=255, unique=True, null=True)
librarything_key = models.CharField(max_length=255, unique=True, null=True)
fedireads_key = models.CharField(max_length=255, unique=True, default=uuid4) fedireads_key = models.CharField(max_length=255, unique=True, default=uuid4)
goodreads_key = models.CharField(max_length=255, unique=True, null=True) openlibrary_key = models.CharField(max_length=255, blank=True, null=True)
librarything_key = models.CharField(max_length=255, blank=True, null=True)
goodreads_key = models.CharField(max_length=255, blank=True, null=True)
misc_identifiers = JSONField(null=True) misc_identifiers = JSONField(null=True)
# info about where the data comes from and where/if to sync # info about where the data comes from and where/if to sync
source_url = models.CharField(max_length=255, unique=True, null=True) source_url = models.CharField(max_length=255, unique=True, null=True)
sync = models.BooleanField(default=True) sync = models.BooleanField(default=True)
sync_cover = models.BooleanField(default=True)
last_sync_date = models.DateTimeField(default=datetime.now) last_sync_date = models.DateTimeField(default=datetime.now)
connector = models.ForeignKey( connector = models.ForeignKey(
'Connector', on_delete=models.PROTECT, null=True) 'Connector', on_delete=models.PROTECT, null=True)
@ -64,10 +65,10 @@ class Book(FedireadsModel):
# book/work metadata # book/work metadata
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
sort_title = models.CharField(max_length=255, null=True) sort_title = models.CharField(max_length=255, blank=True, null=True)
subtitle = models.TextField(blank=True, null=True) subtitle = models.CharField(max_length=255, blank=True, null=True)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
language = models.CharField(max_length=255, null=True) language = models.CharField(max_length=255, blank=True, null=True)
series = models.CharField(max_length=255, blank=True, null=True) series = models.CharField(max_length=255, blank=True, null=True)
series_number = models.CharField(max_length=255, blank=True, null=True) series_number = models.CharField(max_length=255, blank=True, null=True)
subjects = ArrayField( subjects = ArrayField(
@ -78,10 +79,9 @@ class Book(FedireadsModel):
) )
# TODO: include an annotation about the type of authorship (ie, translator) # TODO: include an annotation about the type of authorship (ie, translator)
authors = models.ManyToManyField('Author') authors = models.ManyToManyField('Author')
# TODO: also store cover thumbnail
cover = models.ImageField(upload_to='covers/', blank=True, null=True) cover = models.ImageField(upload_to='covers/', blank=True, null=True)
first_published_date = models.DateTimeField(null=True) first_published_date = models.DateTimeField(blank=True, null=True)
published_date = models.DateTimeField(null=True) published_date = models.DateTimeField(blank=True, null=True)
shelves = models.ManyToManyField( shelves = models.ManyToManyField(
'Shelf', 'Shelf',
symmetrical=False, symmetrical=False,
@ -109,16 +109,16 @@ class Book(FedireadsModel):
class Work(Book): class Work(Book):
''' a work (an abstract concept of a book that manifests in an edition) ''' ''' a work (an abstract concept of a book that manifests in an edition) '''
# library of congress catalog control number # library of congress catalog control number
lccn = models.CharField(max_length=255, unique=True, null=True) lccn = models.CharField(max_length=255, blank=True, null=True)
class Edition(Book): class Edition(Book):
''' an edition of a book ''' ''' an edition of a book '''
# these identifiers only apply to work # these identifiers only apply to work
isbn = models.CharField(max_length=255, unique=True, null=True) isbn = models.CharField(max_length=255, blank=True, null=True)
oclc_number = models.CharField(max_length=255, unique=True, null=True) oclc_number = models.CharField(max_length=255, blank=True, null=True)
pages = models.IntegerField(null=True) pages = models.IntegerField(blank=True, null=True)
physical_format = models.CharField(max_length=255, null=True) physical_format = models.CharField(max_length=255, blank=True, null=True)
publishers = ArrayField( publishers = ArrayField(
models.CharField(max_length=255), blank=True, default=list models.CharField(max_length=255), blank=True, default=list
) )
@ -126,15 +126,15 @@ class Edition(Book):
class Author(FedireadsModel): class Author(FedireadsModel):
''' copy of an author from OL ''' ''' copy of an author from OL '''
openlibrary_key = models.CharField(max_length=255, null=True, unique=True) fedireads_key = models.CharField(max_length=255, unique=True, default=uuid4)
fedireads_key = models.CharField(max_length=255, null=True, unique=True) openlibrary_key = models.CharField(max_length=255, blank=True, null=True)
wikipedia_link = models.CharField(max_length=255, blank=True, null=True) wikipedia_link = models.CharField(max_length=255, blank=True, null=True)
# idk probably other keys would be useful here? # idk probably other keys would be useful here?
born = models.DateTimeField(null=True) born = models.DateTimeField(blank=True, null=True)
died = models.DateTimeField(null=True) died = models.DateTimeField(blank=True, null=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255, null=True) last_name = models.CharField(max_length=255, blank=True, null=True)
first_name = models.CharField(max_length=255, null=True) first_name = models.CharField(max_length=255, blank=True, null=True)
aliases = ArrayField( aliases = ArrayField(
models.CharField(max_length=255), blank=True, default=list models.CharField(max_length=255), blank=True, default=list
) )

View file

@ -281,12 +281,12 @@ button.warning {
height: 5em; height: 5em;
} }
.user-profile h2 a { h2 .edit-link {
text-decoration: none; text-decoration: none;
font-size: 0.9em; font-size: 0.9em;
float: right; float: right;
} }
.user-profile h2 .icon { h2 .edit-link .icon {
font-size: 1.2em; font-size: 1.2em;
} }
.user-profile .row > * { .user-profile .row > * {

View file

@ -1,9 +1,18 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="content-container"> <div class="content-container user-profile">
<h2><q>{{ book.title }}</q> by <h2><q>{{ book.title }}</q> by
{% include 'snippets/authors.html' with book=book %}</h2> {% include 'snippets/authors.html' with book=book %}
{% if request.user.is_authenticated %}
<a href="{{ book.fedireads_key }}/edit" class="edit-link">edit
<span class="icon icon-pencil">
<span class="hidden-text">Edit Book</span>
</span>
</a>
{% endif %}
</h2>
<div> <div>
{% if book.parent_work %}<p>Edition of <a href="/book/{{ book.parent_work.fedireads_key }}">{{ book.parent_work.title }}</a></p>{% endif %} {% if book.parent_work %}<p>Edition of <a href="/book/{{ book.parent_work.fedireads_key }}">{{ book.parent_work.title }}</a></p>{% endif %}
<div class="book-preview"> <div class="book-preview">
@ -24,6 +33,15 @@
{% include 'snippets/shelve_button.html' %} {% include 'snippets/shelve_button.html' %}
</div> </div>
<div>
{% if request.user.is_authenticated and not book.cover %}
<form name="add-cover" method="POST" action="/upload_cover/{{book.id}}" enctype="multipart/form-data">
{% csrf_token %}
{{ cover_form.as_p }}
<button type="submit">Add cover</button>
</form>
{% endif %}
</div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,15 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<h2>Edit "{{ book.title }}"</h2>
<p class="book-cover">{% include 'snippets/book_cover.html' with book=book %}</p>
<form name="edit-book" action="/edit_book/{{ book.id }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Update book</button>
</form>
</div>
{% endblock %}

View file

@ -1,8 +1,11 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div id="content"> <div class="content-container">
<div class="user-profile"> <div class="user-profile">
<h1>{% if user.localname %}{{ user.localname }}{% else %}{{ user.username }}{% endif %}</h1> <h2>Edit Profile</h2>
<p>{% include 'snippets/avatar.html' with user=user %} {% if user.localname %}{{ user.localname }}{% else %}{{ user.username }}{% endif %}</p>
<form name="avatar" action="/edit_profile/" method="post" enctype="multipart/form-data"> <form name="avatar" action="/edit_profile/" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}

View file

@ -2,8 +2,8 @@
{% load fr_display %} {% load fr_display %}
<div class="content-container user-profile"> <div class="content-container user-profile">
<h2>User Profile <h2>User Profile
{% if is_self %} {% if is_self %}
<a href="/user-edit/">edit <a href="/user-edit/" class="edit-link">edit
<span class="icon icon-pencil"> <span class="icon icon-pencil">
<span class="hidden-text">Edit profile</span> <span class="hidden-text">Edit profile</span>
</span> </span>

View file

@ -35,29 +35,27 @@ urlpatterns = [
re_path(r'^notifications/?', views.notifications_page), re_path(r'^notifications/?', views.notifications_page),
re_path(r'books/?$', views.books_page), re_path(r'books/?$', views.books_page),
re_path(r'import/?$', views.import_page), re_path(r'import/?$', views.import_page),
re_path(r'user-edit/?$', views.edit_profile_page),
# should return a ui view or activitypub json blob as requested # should return a ui view or activitypub json blob as requested
# users # users
re_path(r'%s/?$' % user_path, views.user_page), re_path(r'%s/?$' % user_path, views.user_page),
re_path(r'%s/?$' % local_user_path, views.user_page), re_path(r'%s/?$' % local_user_path, views.user_page),
re_path(r'%s\.json$' % local_user_path, views.user_page), re_path(r'%s\.json$' % local_user_path, views.user_page),
re_path(r'user-edit/?$', views.edit_profile_page),
re_path(r'%s/shelves/?$' % local_user_path, views.user_shelves_page), re_path(r'%s/shelves/?$' % local_user_path, views.user_shelves_page),
re_path(r'%s/followers/?$' % local_user_path, views.followers_page), re_path(r'%s/followers(.json)?/?$' % local_user_path, views.followers_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/following/?$' % local_user_path, views.following_page),
re_path(r'%s/following.json$' % local_user_path, views.following_page),
# statuses # statuses
re_path(r'%s/?$' % status_path, views.status_page), re_path(r'%s(.json)?/?$' % status_path, views.status_page),
re_path(r'%s.json$' % status_path, views.status_page),
re_path(r'%s/activity/?$' % status_path, views.status_page), re_path(r'%s/activity/?$' % status_path, views.status_page),
re_path(r'%s/replies/?$' % status_path, views.replies_page), re_path(r'%s/replies(.json)?/?$' % status_path, views.replies_page),
re_path(r'%s/replies\.json$' % status_path, views.replies_page),
# books # books
re_path(r'^book/(?P<book_identifier>[\w\-]+)(.json)?/?$', views.book_page), re_path(r'^book/(?P<book_identifier>[\w\-]+)(.json)?/?$', views.book_page),
re_path(r'^book/(?P<book_identifier>[\w\-]+)/(?P<tab>friends|local|federated)?$', views.book_page), re_path(r'^book/(?P<book_identifier>[\w\-]+)/(?P<tab>friends|local|federated)?$', views.book_page),
re_path(r'^book/(?P<book_identifier>[\w\-]+)/edit/?$', views.edit_book_page),
re_path(r'^author/(?P<author_identifier>\w+)/?$', views.author_page), re_path(r'^author/(?P<author_identifier>\w+)/?$', views.author_page),
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page), re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),
re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % username_regex, views.shelf_page), re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % username_regex, views.shelf_page),
@ -67,23 +65,29 @@ urlpatterns = [
re_path(r'^logout/?$', actions.user_logout), re_path(r'^logout/?$', actions.user_logout),
re_path(r'^user-login/?$', actions.user_login), re_path(r'^user-login/?$', actions.user_login),
re_path(r'^register/?$', actions.register), re_path(r'^register/?$', actions.register),
re_path(r'^edit_profile/?$', actions.edit_profile),
re_path(r'^search/?$', actions.search),
re_path(r'^import_data/?', actions.import_data),
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'^review/?$', actions.review), re_path(r'^review/?$', actions.review),
re_path(r'^comment/?$', actions.comment), re_path(r'^comment/?$', actions.comment),
re_path(r'^tag/?$', actions.tag), re_path(r'^tag/?$', actions.tag),
re_path(r'^untag/?$', actions.untag), re_path(r'^untag/?$', actions.untag),
re_path(r'^reply/?$', actions.reply), re_path(r'^reply/?$', actions.reply),
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite), re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite), re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
re_path(r'^shelve/?$', actions.shelve), re_path(r'^shelve/?$', actions.shelve),
re_path(r'^follow/?$', actions.follow), re_path(r'^follow/?$', actions.follow),
re_path(r'^unfollow/?$', actions.unfollow), re_path(r'^unfollow/?$', actions.unfollow),
re_path(r'^search/?$', actions.search),
re_path(r'^edit_profile/?$', actions.edit_profile),
re_path(r'^clear-notifications/?$', actions.clear_notifications),
re_path(r'^accept_follow_request/?$', actions.accept_follow_request), re_path(r'^accept_follow_request/?$', actions.accept_follow_request),
re_path(r'^delete_follow_request/?$', actions.delete_follow_request), re_path(r'^delete_follow_request/?$', actions.delete_follow_request),
re_path(r'import_data', actions.import_data), re_path(r'^clear-notifications/?$', actions.clear_notifications),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -3,7 +3,7 @@ from io import TextIOWrapper
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
import re import re
@ -87,9 +87,51 @@ def edit_profile(request):
return redirect('/user/%s' % request.user.localname) return redirect('/user/%s' % request.user.localname)
@login_required
def edit_book(request, book_id):
''' edit a book cool '''
if not request.method == 'POST':
return redirect('/book/%s' % request.user.localname)
try:
book = models.Book.objects.get(id=book_id)
except models.Book.DoesNotExist:
return HttpResponseNotFound()
form = forms.BookForm(request.POST, request.FILES, instance=book)
if not form.is_valid():
return redirect(request.headers.get('Referer', '/'))
form.save()
return redirect('/book/%s' % book.fedireads_key)
@login_required
def upload_cover(request, book_id):
''' upload a new cover '''
# TODO: alternate covers?
if not request.method == 'POST':
return redirect('/book/%s' % request.user.localname)
try:
book = models.Book.objects.get(id=book_id)
except models.Book.DoesNotExist:
return HttpResponseNotFound()
form = forms.CoverForm(request.POST, request.FILES, instance=book)
if not form.is_valid():
return redirect(request.headers.get('Referer', '/'))
book.cover = form.files['cover']
book.sync_cover = False
book.save()
return redirect('/book/%s' % book.fedireads_key)
@login_required @login_required
def shelve(request): def shelve(request):
''' put a book on a user's shelf ''' ''' put a on a user's shelf '''
book = models.Book.objects.get(id=request.POST['book']) book = models.Book.objects.get(id=request.POST['book'])
desired_shelf = models.Shelf.objects.filter( desired_shelf = models.Shelf.objects.filter(
identifier=request.POST['shelf'], identifier=request.POST['shelf'],

View file

@ -390,10 +390,22 @@ def book_page(request, book_identifier, tab='friends'):
], ],
'active_tab': tab, 'active_tab': tab,
'path': '/book/%s' % book_identifier, 'path': '/book/%s' % book_identifier,
'cover_form': forms.CoverForm(instance=book),
} }
return TemplateResponse(request, 'book.html', data) return TemplateResponse(request, 'book.html', data)
@login_required
def edit_book_page(request, book_identifier):
''' info about a book '''
book = books_manager.get_or_create_book(book_identifier)
data = {
'book': book,
'form': forms.BookForm(instance=book)
}
return TemplateResponse(request, 'edit_book.html', data)
def author_page(request, author_identifier): def author_page(request, author_identifier):
''' landing page for an author ''' ''' landing page for an author '''
try: try: