mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-23 00:26:33 +00:00
parent
16fec1b6d5
commit
1a33290267
11 changed files with 281 additions and 44 deletions
|
@ -75,5 +75,34 @@ class TagForm(ModelForm):
|
|||
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):
|
||||
csv_file = forms.FileField()
|
||||
|
||||
|
|
114
fedireads/migrations/0023_auto_20200328_2203.py
Normal file
114
fedireads/migrations/0023_auto_20200328_2203.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -47,15 +47,16 @@ class Connector(FedireadsModel):
|
|||
class Book(FedireadsModel):
|
||||
''' a generic book, which can mean either an edition or a work '''
|
||||
# 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)
|
||||
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)
|
||||
|
||||
# info about where the data comes from and where/if to sync
|
||||
source_url = models.CharField(max_length=255, unique=True, null=True)
|
||||
sync = models.BooleanField(default=True)
|
||||
sync_cover = models.BooleanField(default=True)
|
||||
last_sync_date = models.DateTimeField(default=datetime.now)
|
||||
connector = models.ForeignKey(
|
||||
'Connector', on_delete=models.PROTECT, null=True)
|
||||
|
@ -64,10 +65,10 @@ class Book(FedireadsModel):
|
|||
|
||||
# book/work metadata
|
||||
title = models.CharField(max_length=255)
|
||||
sort_title = models.CharField(max_length=255, null=True)
|
||||
subtitle = models.TextField(blank=True, null=True)
|
||||
sort_title = models.CharField(max_length=255, blank=True, null=True)
|
||||
subtitle = models.CharField(max_length=255, 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_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
subjects = ArrayField(
|
||||
|
@ -78,10 +79,9 @@ class Book(FedireadsModel):
|
|||
)
|
||||
# TODO: include an annotation about the type of authorship (ie, translator)
|
||||
authors = models.ManyToManyField('Author')
|
||||
# TODO: also store cover thumbnail
|
||||
cover = models.ImageField(upload_to='covers/', blank=True, null=True)
|
||||
first_published_date = models.DateTimeField(null=True)
|
||||
published_date = models.DateTimeField(null=True)
|
||||
first_published_date = models.DateTimeField(blank=True, null=True)
|
||||
published_date = models.DateTimeField(blank=True, null=True)
|
||||
shelves = models.ManyToManyField(
|
||||
'Shelf',
|
||||
symmetrical=False,
|
||||
|
@ -109,16 +109,16 @@ class Book(FedireadsModel):
|
|||
class Work(Book):
|
||||
''' a work (an abstract concept of a book that manifests in an edition) '''
|
||||
# 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):
|
||||
''' an edition of a book '''
|
||||
# these identifiers only apply to work
|
||||
isbn = models.CharField(max_length=255, unique=True, null=True)
|
||||
oclc_number = models.CharField(max_length=255, unique=True, null=True)
|
||||
pages = models.IntegerField(null=True)
|
||||
physical_format = models.CharField(max_length=255, null=True)
|
||||
isbn = models.CharField(max_length=255, blank=True, null=True)
|
||||
oclc_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
pages = models.IntegerField(blank=True, null=True)
|
||||
physical_format = models.CharField(max_length=255, blank=True, null=True)
|
||||
publishers = ArrayField(
|
||||
models.CharField(max_length=255), blank=True, default=list
|
||||
)
|
||||
|
@ -126,15 +126,15 @@ class Edition(Book):
|
|||
|
||||
class Author(FedireadsModel):
|
||||
''' copy of an author from OL '''
|
||||
openlibrary_key = models.CharField(max_length=255, null=True, unique=True)
|
||||
fedireads_key = models.CharField(max_length=255, null=True, unique=True)
|
||||
fedireads_key = models.CharField(max_length=255, unique=True, default=uuid4)
|
||||
openlibrary_key = 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?
|
||||
born = models.DateTimeField(null=True)
|
||||
died = models.DateTimeField(null=True)
|
||||
born = models.DateTimeField(blank=True, null=True)
|
||||
died = models.DateTimeField(blank=True, null=True)
|
||||
name = models.CharField(max_length=255)
|
||||
last_name = models.CharField(max_length=255, null=True)
|
||||
first_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, blank=True, null=True)
|
||||
aliases = ArrayField(
|
||||
models.CharField(max_length=255), blank=True, default=list
|
||||
)
|
||||
|
|
|
@ -281,12 +281,12 @@ button.warning {
|
|||
height: 5em;
|
||||
}
|
||||
|
||||
.user-profile h2 a {
|
||||
h2 .edit-link {
|
||||
text-decoration: none;
|
||||
font-size: 0.9em;
|
||||
float: right;
|
||||
}
|
||||
.user-profile h2 .icon {
|
||||
h2 .edit-link .icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.user-profile .row > * {
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load fr_display %}
|
||||
{% block content %}
|
||||
<div class="content-container">
|
||||
<div class="content-container user-profile">
|
||||
<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>
|
||||
{% 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">
|
||||
|
@ -24,6 +33,15 @@
|
|||
{% include 'snippets/shelve_button.html' %}
|
||||
|
||||
</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>
|
||||
|
||||
|
|
15
fedireads/templates/edit_book.html
Normal file
15
fedireads/templates/edit_book.html
Normal 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 %}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block content %}
|
||||
<div id="content">
|
||||
<div class="content-container">
|
||||
<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">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
{% load fr_display %}
|
||||
<div class="content-container user-profile">
|
||||
<h2>User Profile
|
||||
{% if is_self %}
|
||||
<a href="/user-edit/">edit
|
||||
{% if is_self %}
|
||||
<a href="/user-edit/" class="edit-link">edit
|
||||
<span class="icon icon-pencil">
|
||||
<span class="hidden-text">Edit profile</span>
|
||||
</span>
|
||||
|
|
|
@ -35,29 +35,27 @@ urlpatterns = [
|
|||
re_path(r'^notifications/?', views.notifications_page),
|
||||
re_path(r'books/?$', views.books_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
|
||||
# users
|
||||
re_path(r'%s/?$' % 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'user-edit/?$', views.edit_profile_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/following/?$' % local_user_path, views.following_page),
|
||||
re_path(r'%s/following.json$' % local_user_path, views.following_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),
|
||||
|
||||
# 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/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
|
||||
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\-]+)/edit/?$', views.edit_book_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'^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'^user-login/?$', actions.user_login),
|
||||
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'^comment/?$', actions.comment),
|
||||
re_path(r'^tag/?$', actions.tag),
|
||||
re_path(r'^untag/?$', actions.untag),
|
||||
re_path(r'^reply/?$', actions.reply),
|
||||
|
||||
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
|
||||
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
|
||||
|
||||
re_path(r'^shelve/?$', actions.shelve),
|
||||
|
||||
re_path(r'^follow/?$', actions.follow),
|
||||
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'^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)
|
||||
|
|
|
@ -3,7 +3,7 @@ from io import TextIOWrapper
|
|||
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
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.template.response import TemplateResponse
|
||||
import re
|
||||
|
@ -87,9 +87,51 @@ def edit_profile(request):
|
|||
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
|
||||
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'])
|
||||
desired_shelf = models.Shelf.objects.filter(
|
||||
identifier=request.POST['shelf'],
|
||||
|
|
|
@ -390,10 +390,22 @@ def book_page(request, book_identifier, tab='friends'):
|
|||
],
|
||||
'active_tab': tab,
|
||||
'path': '/book/%s' % book_identifier,
|
||||
'cover_form': forms.CoverForm(instance=book),
|
||||
}
|
||||
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):
|
||||
''' landing page for an author '''
|
||||
try:
|
||||
|
|
Loading…
Reference in a new issue