mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-04 23:36:32 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
2d21a31c13
34 changed files with 767 additions and 367 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,7 +1,7 @@
|
||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
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
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
|
|
@ -3,7 +3,7 @@ from dataclasses import dataclass, fields, MISSING
|
||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
|
|
||||||
from django.apps import apps
|
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.connectors import ConnectorException, get_data
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
@ -92,7 +92,10 @@ class ActivityObject:
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# we can't set many to many and reverse fields on an unsaved object
|
# 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
|
# add many to many fields, which have to be set post-save
|
||||||
for field in instance.many_to_many_fields:
|
for field in instance.many_to_many_fields:
|
||||||
|
@ -108,15 +111,10 @@ class ActivityObject:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
model_field = getattr(model, model_field_name)
|
model_field = getattr(model, model_field_name)
|
||||||
try:
|
# creating a Work, model_field is 'editions'
|
||||||
# this is for one to many
|
# creating a User, model field is 'key_pair'
|
||||||
related_model = model_field.field.model
|
related_model = model_field.field.model
|
||||||
related_field_name = model_field.field.name
|
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]
|
|
||||||
|
|
||||||
for item in values:
|
for item in values:
|
||||||
set_related_field.delay(
|
set_related_field.delay(
|
||||||
|
@ -139,8 +137,8 @@ class ActivityObject:
|
||||||
@app.task
|
@app.task
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def set_related_field(
|
def set_related_field(
|
||||||
model_name, origin_model_name,
|
model_name, origin_model_name, related_field_name,
|
||||||
related_field_name, related_remote_id, data):
|
related_remote_id, data):
|
||||||
''' load reverse related fields (editions, attachments) without blocking '''
|
''' load reverse related fields (editions, attachments) without blocking '''
|
||||||
model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True)
|
model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True)
|
||||||
origin_model = apps.get_model(
|
origin_model = apps.get_model(
|
||||||
|
@ -150,23 +148,36 @@ def set_related_field(
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
item = resolve_remote_id(model, data, save=False)
|
existing = model.find_existing_by_remote_id(data)
|
||||||
else:
|
if existing:
|
||||||
# look for a match based on all the available data
|
data = existing.to_activity()
|
||||||
item = model.find_existing(data)
|
else:
|
||||||
if not item:
|
data = get_data(data)
|
||||||
# create a new model instance
|
activity = model.activity_serializer(**data)
|
||||||
item = model.activity_serializer(**data)
|
|
||||||
item = item.to_model(model, save=False)
|
|
||||||
# this must exist because it's the object that triggered this function
|
# this must exist because it's the object that triggered this function
|
||||||
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Invalid related remote id: %s' % related_remote_id)
|
'Invalid related remote id: %s' % related_remote_id)
|
||||||
|
|
||||||
# edition.parent_work = instance, for example
|
# set the origin's remote id on the activity so it will be there when
|
||||||
setattr(item, related_field_name, instance)
|
# the model instance is created
|
||||||
item.save()
|
# 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):
|
def resolve_remote_id(model, remote_id, refresh=False, save=True):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -168,7 +168,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
''' every work needs at least one edition '''
|
''' every work needs at least one edition '''
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_work_from_edition_date(self, data):
|
def get_work_from_edition_data(self, data):
|
||||||
''' every edition needs a work '''
|
''' every edition needs a work '''
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -228,6 +228,7 @@ class SearchResult:
|
||||||
key: str
|
key: str
|
||||||
author: str
|
author: str
|
||||||
year: str
|
year: str
|
||||||
|
connector: object
|
||||||
confidence: int = 1
|
confidence: int = 1
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
|
@ -13,4 +13,5 @@ class Connector(AbstractMinimalConnector):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
|
search_result['connector'] = self
|
||||||
return SearchResult(**search_result)
|
return SearchResult(**search_result)
|
||||||
|
|
|
@ -85,7 +85,7 @@ class Connector(AbstractConnector):
|
||||||
return pick_default_edition(data['entries'])
|
return pick_default_edition(data['entries'])
|
||||||
|
|
||||||
|
|
||||||
def get_work_from_edition_date(self, data):
|
def get_work_from_edition_data(self, data):
|
||||||
try:
|
try:
|
||||||
key = data['works'][0]['key']
|
key = data['works'][0]['key']
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
|
@ -123,6 +123,7 @@ class Connector(AbstractConnector):
|
||||||
title=search_result.get('title'),
|
title=search_result.get('title'),
|
||||||
key=key,
|
key=key,
|
||||||
author=', '.join(author),
|
author=', '.join(author),
|
||||||
|
connector=self,
|
||||||
year=search_result.get('first_publish_year'),
|
year=search_result.get('first_publish_year'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -51,28 +51,23 @@ class Connector(AbstractConnector):
|
||||||
author=search_result.author_text,
|
author=search_result.author_text,
|
||||||
year=search_result.published_date.year if \
|
year=search_result.published_date.year if \
|
||||||
search_result.published_date else None,
|
search_result.published_date else None,
|
||||||
|
connector=self,
|
||||||
confidence=search_result.rank,
|
confidence=search_result.rank,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_remote_id_from_data(self, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_edition_from_work_data(self, data):
|
def get_edition_from_work_data(self, data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_work_from_edition_date(self, data):
|
def get_work_from_edition_data(self, data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_authors_from_data(self, data):
|
def get_authors_from_data(self, data):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_cover_from_data(self, data):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
''' it's already in the right format, don't even worry about it '''
|
''' it's already in the right format, don't even worry about it '''
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -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()
|
||||||
|
|
61
bookwyrm/migrations/0029_auto_20201221_2014.py
Normal file
61
bookwyrm/migrations/0029_auto_20201221_2014.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,7 +2,7 @@
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .book import Book, Work, Edition
|
from .book import Book, Work, Edition, BookDataModel
|
||||||
from .author import Author
|
from .author import Author
|
||||||
from .connector import Connector
|
from .connector import Connector
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -48,9 +59,7 @@ class Book(ActivitypubMixin, BookWyrmModel):
|
||||||
subject_places = fields.ArrayField(
|
subject_places = fields.ArrayField(
|
||||||
models.CharField(max_length=255), blank=True, null=True, default=list
|
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')
|
authors = fields.ManyToManyField('Author')
|
||||||
# preformatted authorship string for search and easier display
|
|
||||||
cover = fields.ImageField(
|
cover = fields.ImageField(
|
||||||
upload_to='covers/', blank=True, null=True, alt_field='alt_text')
|
upload_to='covers/', blank=True, null=True, alt_field='alt_text')
|
||||||
first_published_date = fields.DateTimeField(blank=True, null=True)
|
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 '''
|
''' 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):
|
||||||
|
|
|
@ -76,7 +76,7 @@ class ImportItem(models.Model):
|
||||||
)
|
)
|
||||||
if search_result:
|
if search_result:
|
||||||
# raises ConnectorException
|
# 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
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class ImportItem(models.Model):
|
||||||
)
|
)
|
||||||
if search_result:
|
if search_result:
|
||||||
# raises ConnectorException
|
# 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
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
|
||||||
|
|
||||||
|
|
||||||
def to_reject_activity(self):
|
def to_reject_activity(self):
|
||||||
''' generate an Accept for this follow request '''
|
''' generate a Reject for this follow request '''
|
||||||
return activitypub.Reject(
|
return activitypub.Reject(
|
||||||
id=self.get_remote_id(status='rejects'),
|
id=self.get_remote_id(status='rejects'),
|
||||||
actor=self.user_object.remote_id,
|
actor=self.user_object.remote_id,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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">
|
||||||
|
|
89
bookwyrm/templates/edit_author.html
Normal file
89
bookwyrm/templates/edit_author.html
Normal 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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -44,18 +44,28 @@
|
||||||
<div>
|
<div>
|
||||||
<input class="toggle-control" type="radio" name="recent-books" id="book-{{ book.id }}" {% if shelf_counter == 1 and forloop.first %}checked{% endif %}>
|
<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="toggle-content hidden" role="tabpanel" id="book-{{ book.id }}-panel">
|
||||||
<div class="box">
|
<div class="card">
|
||||||
<div class="block">
|
<div class="card-header">
|
||||||
{% include 'snippets/book_titleby.html' with book=book %}
|
<p class="card-header-title">
|
||||||
{% include 'snippets/shelve_button.html' with book=book %}
|
<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>
|
</div>
|
||||||
{% include 'snippets/create_status.html' with book=book %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div>
|
||||||
|
<input class="toggle-control" type="radio" name="recent-books" id="no-book">
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
<div class="cover-container is-{{ size }}">
|
<div class="cover-container is-{{ size }}">
|
||||||
{% if book.cover %}
|
{% 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 %}
|
{% else %}
|
||||||
<div class="no-cover book-cover">
|
<div class="no-cover book-cover">
|
||||||
<img class="book-cover" src="/static/images/no_cover.jpg" alt="No cover">
|
<img class="book-cover" src="/static/images/no_cover.jpg" alt="No cover">
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
<span>
|
<a href="/book/{{ book.id }}">{{ book.title }}</a>
|
||||||
<a href="/book/{{ book.id }}">{{ book.title }}</a>
|
|
||||||
</span>
|
|
||||||
{% if book.authors %}
|
{% if book.authors %}
|
||||||
<span>
|
by {% include 'snippets/authors.html' with book=book %}
|
||||||
by {% include 'snippets/authors.html' with book=book %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<figure class="image is-128x128">
|
<figure class="image is-128x128">
|
||||||
<a href="/images/{{ attachment.image }}" target="_blank" aria-label="open image in new window">
|
<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>
|
</a>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
|
|
52
bookwyrm/tests/data/ap_generated_shelve_note.json
Normal file
52
bookwyrm/tests/data/ap_generated_shelve_note.json
Normal 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"
|
||||||
|
}
|
|
@ -1,9 +1,15 @@
|
||||||
''' testing models '''
|
''' testing models '''
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.test import TestCase
|
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):
|
class ImportJob(TestCase):
|
||||||
|
@ -54,11 +60,11 @@ class ImportJob(TestCase):
|
||||||
user = models.User.objects.create_user(
|
user = models.User.objects.create_user(
|
||||||
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True)
|
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True)
|
||||||
job = models.ImportJob.objects.create(user=user)
|
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)
|
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)
|
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)
|
job=job, index=3, data=unknown_read_data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,8 +78,7 @@ class ImportJob(TestCase):
|
||||||
def test_shelf(self):
|
def test_shelf(self):
|
||||||
''' converts to the local shelf typology '''
|
''' converts to the local shelf typology '''
|
||||||
expected = 'reading'
|
expected = 'reading'
|
||||||
item = models.ImportItem.objects.get(index=1)
|
self.assertEqual(self.item_1.shelf, expected)
|
||||||
self.assertEqual(item.shelf, expected)
|
|
||||||
|
|
||||||
|
|
||||||
def test_date_added(self):
|
def test_date_added(self):
|
||||||
|
@ -91,21 +96,75 @@ class ImportJob(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_currently_reading_reads(self):
|
def test_currently_reading_reads(self):
|
||||||
|
''' infer currently reading dates where available '''
|
||||||
expected = [models.ReadThrough(
|
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)
|
actual = models.ImportItem.objects.get(index=1)
|
||||||
self.assertEqual(actual.reads[0].start_date, expected[0].start_date)
|
self.assertEqual(actual.reads[0].start_date, expected[0].start_date)
|
||||||
self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date)
|
self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date)
|
||||||
|
|
||||||
def test_read_reads(self):
|
def test_read_reads(self):
|
||||||
actual = models.ImportItem.objects.get(index=2)
|
''' infer read dates where available '''
|
||||||
self.assertEqual(actual.reads[0].start_date, datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc))
|
actual = self.item_2
|
||||||
self.assertEqual(actual.reads[0].finish_date, datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc))
|
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):
|
def test_unread_reads(self):
|
||||||
|
''' handle books with no read dates '''
|
||||||
expected = []
|
expected = []
|
||||||
actual = models.ImportItem.objects.get(index=3)
|
actual = models.ImportItem.objects.get(index=3)
|
||||||
self.assertEqual(actual.reads, expected)
|
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')
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
from . import *
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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')
|
|
|
@ -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)
|
|
|
@ -8,6 +8,7 @@ from django.http import HttpResponseBadRequest, HttpResponseNotAllowed, \
|
||||||
HttpResponseNotFound
|
HttpResponseNotFound
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
import responses
|
||||||
|
|
||||||
from bookwyrm import models, incoming
|
from bookwyrm import models, incoming
|
||||||
|
|
||||||
|
@ -421,6 +422,25 @@ class Incoming(TestCase):
|
||||||
self.assertEqual(notification.related_status, self.status)
|
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):
|
def test_handle_unboost(self):
|
||||||
''' undo a boost '''
|
''' undo a boost '''
|
||||||
activity = {
|
activity = {
|
||||||
|
|
193
bookwyrm/tests/test_outgoing.py
Normal file
193
bookwyrm/tests/test_outgoing.py
Normal 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)
|
|
@ -2,11 +2,13 @@
|
||||||
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
|
||||||
|
|
||||||
from bookwyrm import view_actions as actions, models
|
from bookwyrm import forms, models, view_actions as actions
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -238,6 +247,43 @@ class ViewActions(TestCase):
|
||||||
self.assertEqual(resp.template_name, 'password_reset.html')
|
self.assertEqual(resp.template_name, 'password_reset.html')
|
||||||
self.assertTrue(models.PasswordReset.objects.exists())
|
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):
|
def test_switch_edition(self):
|
||||||
''' updates user's relationships to a book '''
|
''' updates user's relationships to a book '''
|
||||||
work = models.Work.objects.create(title='test work')
|
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.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')
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -159,23 +159,21 @@ def password_change(request):
|
||||||
request.user.set_password(new_password)
|
request.user.set_password(new_password)
|
||||||
request.user.save()
|
request.user.save()
|
||||||
login(request, request.user)
|
login(request, request.user)
|
||||||
return redirect('/user-edit')
|
return redirect('/user/%s' % request.user.localname)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def edit_profile(request):
|
def edit_profile(request):
|
||||||
''' les get fancy with images '''
|
''' 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():
|
if not form.is_valid():
|
||||||
data = {
|
data = {'form': form, 'user': request.user}
|
||||||
'form': form,
|
|
||||||
'user': request.user,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'edit_user.html', data)
|
return TemplateResponse(request, 'edit_user.html', data)
|
||||||
|
|
||||||
request.user.name = form.data['name']
|
user = form.save(commit=False)
|
||||||
request.user.email = form.data['email']
|
|
||||||
if 'avatar' in form.files:
|
if 'avatar' in form.files:
|
||||||
# crop and resize avatar upload
|
# crop and resize avatar upload
|
||||||
image = Image.open(form.files['avatar'])
|
image = Image.open(form.files['avatar'])
|
||||||
|
@ -201,17 +199,10 @@ def edit_profile(request):
|
||||||
# set the name to a hash
|
# set the name to a hash
|
||||||
extension = form.files['avatar'].name.split('.')[-1]
|
extension = form.files['avatar'].name.split('.')[-1]
|
||||||
filename = '%s.%s' % (uuid4(), extension)
|
filename = '%s.%s' % (uuid4(), extension)
|
||||||
request.user.avatar.save(
|
user.avatar.save(filename, ContentFile(output.getvalue()))
|
||||||
filename,
|
user.save()
|
||||||
ContentFile(output.getvalue())
|
|
||||||
)
|
|
||||||
|
|
||||||
request.user.summary = form.data['summary']
|
outgoing.handle_update_user(user)
|
||||||
request.user.manually_approves_followers = \
|
|
||||||
form.cleaned_data['manually_approves_followers']
|
|
||||||
request.user.save()
|
|
||||||
|
|
||||||
outgoing.handle_update_user(request.user)
|
|
||||||
return redirect('/user/%s' % request.user.localname)
|
return redirect('/user/%s' % request.user.localname)
|
||||||
|
|
||||||
|
|
||||||
|
@ -244,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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -289,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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -311,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):
|
||||||
|
|
|
@ -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 '''
|
||||||
|
|
Loading…
Reference in a new issue