Merge branch 'main' into logo-default
This commit is contained in:
commit
ae07bbffb7
230 changed files with 8169 additions and 4734 deletions
|
@ -2,15 +2,19 @@
|
|||
import inspect
|
||||
import sys
|
||||
|
||||
from .base_activity import ActivityEncoder, Image, PublicKey, Signature
|
||||
from .base_activity import ActivityEncoder, Signature
|
||||
from .base_activity import Link, Mention
|
||||
from .base_activity import ActivitySerializerError, resolve_remote_id
|
||||
from .image import Image
|
||||
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
|
||||
from .note import Tombstone
|
||||
from .interaction import Boost, Like
|
||||
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
||||
from .person import Person
|
||||
from .person import Person, PublicKey
|
||||
from .book import Edition, Work, Author
|
||||
from .verbs import Create, Undo, Update
|
||||
from .verbs import Create, Delete, Undo, Update
|
||||
from .verbs import Follow, Accept, Reject
|
||||
from .verbs import Add, Remove
|
||||
from .verbs import Add, AddBook, Remove
|
||||
|
||||
# this creates a list of all the Activity types that we can serialize,
|
||||
# so when an Activity comes in from outside, we can check if it's known
|
||||
|
|
|
@ -2,8 +2,16 @@
|
|||
from dataclasses import dataclass, fields, MISSING
|
||||
from json import JSONEncoder
|
||||
|
||||
from django.db.models.fields.related_descriptors \
|
||||
import ForwardManyToOneDescriptor
|
||||
from django.apps import apps
|
||||
from django.db import transaction
|
||||
from django.db.models.fields.files import ImageFileDescriptor
|
||||
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
|
||||
|
||||
from bookwyrm.connectors import ConnectorException, get_data
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
class ActivitySerializerError(ValueError):
|
||||
''' routine problems serializing activitypub json '''
|
||||
|
||||
|
||||
class ActivityEncoder(JSONEncoder):
|
||||
|
@ -13,19 +21,17 @@ class ActivityEncoder(JSONEncoder):
|
|||
|
||||
|
||||
@dataclass
|
||||
class Image:
|
||||
''' image block '''
|
||||
mediaType: str
|
||||
url: str
|
||||
type: str = 'Image'
|
||||
class Link:
|
||||
''' for tagging a book in a status '''
|
||||
href: str
|
||||
name: str
|
||||
type: str = 'Link'
|
||||
|
||||
|
||||
@dataclass
|
||||
class PublicKey:
|
||||
''' public key block '''
|
||||
id: str
|
||||
owner: str
|
||||
publicKeyPem: str
|
||||
class Mention(Link):
|
||||
''' a subtype of Link for mentioning an actor '''
|
||||
type: str = 'Mention'
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -44,55 +50,104 @@ class ActivityObject:
|
|||
type: str
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
''' this lets you pass in an object with fields
|
||||
that aren't in the dataclass, which it ignores.
|
||||
Any field in the dataclass is required or has a
|
||||
default value '''
|
||||
''' this lets you pass in an object with fields that aren't in the
|
||||
dataclass, which it ignores. Any field in the dataclass is required or
|
||||
has a default value '''
|
||||
for field in fields(self):
|
||||
try:
|
||||
value = kwargs[field.name]
|
||||
except KeyError:
|
||||
if field.default == MISSING:
|
||||
raise TypeError('Missing required field: %s' % field.name)
|
||||
if field.default == MISSING and \
|
||||
field.default_factory == MISSING:
|
||||
raise ActivitySerializerError(\
|
||||
'Missing required field: %s' % field.name)
|
||||
value = field.default
|
||||
setattr(self, field.name, value)
|
||||
|
||||
|
||||
def to_model(self, model, instance=None):
|
||||
''' convert from an activity to a model '''
|
||||
@transaction.atomic
|
||||
def to_model(self, model, instance=None, save=True):
|
||||
''' convert from an activity to a model instance '''
|
||||
if not isinstance(self, model.activity_serializer):
|
||||
raise TypeError('Wrong activity type for model')
|
||||
raise ActivitySerializerError(
|
||||
'Wrong activity type "%s" for model "%s" (expects "%s")' % \
|
||||
(self.__class__,
|
||||
model.__name__,
|
||||
model.activity_serializer)
|
||||
)
|
||||
|
||||
model_fields = [m.name for m in model._meta.get_fields()]
|
||||
mapped_fields = {}
|
||||
# check for an existing instance, if we're not updating a known obj
|
||||
if not instance:
|
||||
instance = model.find_existing(self.serialize()) or model()
|
||||
|
||||
for mapping in model.activity_mappings:
|
||||
if mapping.model_key not in model_fields:
|
||||
many_to_many_fields = {}
|
||||
image_fields = {}
|
||||
for field in model._meta.get_fields():
|
||||
# check if it's an activitypub field
|
||||
if not hasattr(field, 'field_to_activity'):
|
||||
continue
|
||||
# call the formatter associated with the model field class
|
||||
value = field.field_from_activity(
|
||||
getattr(self, field.get_activitypub_field())
|
||||
)
|
||||
if value is None or value is MISSING:
|
||||
continue
|
||||
# value is None if there's a default that isn't supplied
|
||||
# in the activity but is supplied in the formatter
|
||||
value = None
|
||||
if mapping.activity_key:
|
||||
value = getattr(self, mapping.activity_key)
|
||||
model_field = getattr(model, mapping.model_key)
|
||||
|
||||
# remote_id -> foreign key resolver
|
||||
if isinstance(model_field, ForwardManyToOneDescriptor) and value:
|
||||
fk_model = model_field.field.related_model
|
||||
value = resolve_foreign_key(fk_model, value)
|
||||
model_field = getattr(model, field.name)
|
||||
|
||||
mapped_fields[mapping.model_key] = mapping.model_formatter(value)
|
||||
if isinstance(model_field, ManyToManyDescriptor):
|
||||
# status mentions book/users for example, stash this for later
|
||||
many_to_many_fields[field.name] = value
|
||||
elif isinstance(model_field, ImageFileDescriptor):
|
||||
# image fields need custom handling
|
||||
image_fields[field.name] = value
|
||||
else:
|
||||
# just a good old fashioned model.field = value
|
||||
setattr(instance, field.name, value)
|
||||
|
||||
# if this isn't here, it messes up saving users. who even knows.
|
||||
for (model_key, value) in image_fields.items():
|
||||
getattr(instance, model_key).save(*value, save=save)
|
||||
|
||||
# updating an existing model isntance
|
||||
if instance:
|
||||
for k, v in mapped_fields.items():
|
||||
setattr(instance, k, v)
|
||||
instance.save()
|
||||
if not save:
|
||||
# we can't set many to many and reverse fields on an unsaved object
|
||||
return instance
|
||||
|
||||
# creating a new model instance
|
||||
return model.objects.create(**mapped_fields)
|
||||
instance.save()
|
||||
|
||||
# add many to many fields, which have to be set post-save
|
||||
for (model_key, values) in many_to_many_fields.items():
|
||||
# mention books/users, for example
|
||||
getattr(instance, model_key).set(values)
|
||||
|
||||
if not save or not hasattr(model, 'deserialize_reverse_fields'):
|
||||
return instance
|
||||
|
||||
# reversed relationships in the models
|
||||
for (model_field_name, activity_field_name) in \
|
||||
model.deserialize_reverse_fields:
|
||||
# attachments on Status, for example
|
||||
values = getattr(self, activity_field_name)
|
||||
if values is None or values is MISSING:
|
||||
continue
|
||||
try:
|
||||
# this is for one to many
|
||||
related_model = getattr(model, model_field_name).field.model
|
||||
except AttributeError:
|
||||
# it's a one to one or foreign key
|
||||
related_model = getattr(model, model_field_name)\
|
||||
.related.related_model
|
||||
values = [values]
|
||||
|
||||
for item in values:
|
||||
set_related_field.delay(
|
||||
related_model.__name__,
|
||||
instance.__class__.__name__,
|
||||
instance.__class__.__name__.lower(),
|
||||
instance.remote_id,
|
||||
item
|
||||
)
|
||||
return instance
|
||||
|
||||
|
||||
def serialize(self):
|
||||
|
@ -102,17 +157,57 @@ class ActivityObject:
|
|||
return data
|
||||
|
||||
|
||||
def resolve_foreign_key(model, remote_id):
|
||||
''' look up the remote_id on an activity json field '''
|
||||
result = model.objects
|
||||
if hasattr(model.objects, 'select_subclasses'):
|
||||
result = result.select_subclasses()
|
||||
@app.task
|
||||
@transaction.atomic
|
||||
def set_related_field(
|
||||
model_name, origin_model_name,
|
||||
related_field_name, related_remote_id, data):
|
||||
''' load reverse related fields (editions, attachments) without blocking '''
|
||||
model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True)
|
||||
origin_model = apps.get_model(
|
||||
'bookwyrm.%s' % origin_model_name,
|
||||
require_ready=True
|
||||
)
|
||||
|
||||
result = result.filter(
|
||||
remote_id=remote_id
|
||||
).first()
|
||||
if isinstance(data, str):
|
||||
item = resolve_remote_id(model, data, save=False)
|
||||
else:
|
||||
# look for a match based on all the available data
|
||||
item = model.find_existing(data)
|
||||
if not item:
|
||||
# create a new model instance
|
||||
item = model.activity_serializer(**data)
|
||||
item = item.to_model(model, save=False)
|
||||
# this must exist because it's the object that triggered this function
|
||||
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
||||
if not instance:
|
||||
raise ValueError('Invalid related remote id: %s' % related_remote_id)
|
||||
|
||||
if not result:
|
||||
raise ValueError('Could not resolve remote_id in %s model: %s' % \
|
||||
# edition.parent_work = instance, for example
|
||||
setattr(item, related_field_name, instance)
|
||||
item.save()
|
||||
|
||||
|
||||
def resolve_remote_id(model, remote_id, refresh=False, save=True):
|
||||
''' take a remote_id and return an instance, creating if necessary '''
|
||||
result = model.find_existing_by_remote_id(remote_id)
|
||||
if result and not refresh:
|
||||
return result
|
||||
|
||||
# load the data and create the object
|
||||
try:
|
||||
data = get_data(remote_id)
|
||||
except (ConnectorException, ConnectionError):
|
||||
raise ActivitySerializerError(
|
||||
'Could not connect to host for remote_id in %s model: %s' % \
|
||||
(model.__name__, remote_id))
|
||||
return result
|
||||
|
||||
# check for existing items with shared unique identifiers
|
||||
if not result:
|
||||
result = model.find_existing(data)
|
||||
if result and not refresh:
|
||||
return result
|
||||
|
||||
item = model.activity_serializer(**data)
|
||||
# if we're refreshing, "result" will be set and we'll update it
|
||||
return item.to_model(model, instance=result, save=save)
|
||||
|
|
|
@ -2,66 +2,66 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from .base_activity import ActivityObject, Image
|
||||
from .base_activity import ActivityObject
|
||||
from .image import Image
|
||||
|
||||
@dataclass(init=False)
|
||||
class Book(ActivityObject):
|
||||
''' serializes an edition or work, abstract '''
|
||||
authors: List[str]
|
||||
first_published_date: str
|
||||
published_date: str
|
||||
|
||||
title: str
|
||||
sort_title: str
|
||||
subtitle: str
|
||||
description: str
|
||||
languages: List[str]
|
||||
series: str
|
||||
series_number: str
|
||||
subjects: List[str]
|
||||
subject_places: List[str]
|
||||
sortTitle: str = ''
|
||||
subtitle: str = ''
|
||||
description: str = ''
|
||||
languages: List[str] = field(default_factory=lambda: [])
|
||||
series: str = ''
|
||||
seriesNumber: str = ''
|
||||
subjects: List[str] = field(default_factory=lambda: [])
|
||||
subjectPlaces: List[str] = field(default_factory=lambda: [])
|
||||
|
||||
openlibrary_key: str
|
||||
librarything_key: str
|
||||
goodreads_key: str
|
||||
authors: List[str] = field(default_factory=lambda: [])
|
||||
firstPublishedDate: str = ''
|
||||
publishedDate: str = ''
|
||||
|
||||
attachment: List[Image] = field(default=lambda: [])
|
||||
openlibraryKey: str = ''
|
||||
librarythingKey: str = ''
|
||||
goodreadsKey: str = ''
|
||||
|
||||
cover: Image = field(default_factory=lambda: {})
|
||||
type: str = 'Book'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Edition(Book):
|
||||
''' Edition instance of a book object '''
|
||||
isbn_10: str
|
||||
isbn_13: str
|
||||
oclc_number: str
|
||||
asin: str
|
||||
pages: str
|
||||
physical_format: str
|
||||
publishers: List[str]
|
||||
|
||||
work: str
|
||||
isbn10: str = ''
|
||||
isbn13: str = ''
|
||||
oclcNumber: str = ''
|
||||
asin: str = ''
|
||||
pages: str = ''
|
||||
physicalFormat: str = ''
|
||||
publishers: List[str] = field(default_factory=lambda: [])
|
||||
|
||||
type: str = 'Edition'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Work(Book):
|
||||
''' work instance of a book object '''
|
||||
lccn: str
|
||||
lccn: str = ''
|
||||
defaultEdition: str = ''
|
||||
editions: List[str]
|
||||
type: str = 'Work'
|
||||
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Author(ActivityObject):
|
||||
''' author of a book '''
|
||||
url: str
|
||||
name: str
|
||||
born: str
|
||||
died: str
|
||||
aliases: str
|
||||
bio: str
|
||||
openlibrary_key: str
|
||||
wikipedia_link: str
|
||||
born: str = ''
|
||||
died: str = ''
|
||||
aliases: str = ''
|
||||
bio: str = ''
|
||||
openlibraryKey: str = ''
|
||||
wikipediaLink: str = ''
|
||||
type: str = 'Person'
|
||||
|
|
11
bookwyrm/activitypub/image.py
Normal file
11
bookwyrm/activitypub/image.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
''' an image, nothing fancy '''
|
||||
from dataclasses import dataclass
|
||||
from .base_activity import ActivityObject
|
||||
|
||||
@dataclass(init=False)
|
||||
class Image(ActivityObject):
|
||||
''' image block '''
|
||||
url: str
|
||||
name: str = ''
|
||||
type: str = 'Image'
|
||||
id: str = ''
|
|
@ -2,21 +2,29 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
|
||||
from .base_activity import ActivityObject, Image
|
||||
from .base_activity import ActivityObject, Link
|
||||
from .image import Image
|
||||
|
||||
@dataclass(init=False)
|
||||
class Tombstone(ActivityObject):
|
||||
''' the placeholder for a deleted status '''
|
||||
published: str
|
||||
deleted: str
|
||||
type: str = 'Tombstone'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Note(ActivityObject):
|
||||
''' Note activity '''
|
||||
url: str
|
||||
inReplyTo: str
|
||||
published: str
|
||||
attributedTo: str
|
||||
to: List[str]
|
||||
cc: List[str]
|
||||
content: str
|
||||
replies: Dict
|
||||
# TODO: this is wrong???
|
||||
attachment: List[Image] = field(default=lambda: [])
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
replies: Dict = field(default_factory=lambda: {})
|
||||
inReplyTo: str = ''
|
||||
tag: List[Link] = field(default_factory=lambda: [])
|
||||
attachment: List[Image] = field(default_factory=lambda: [])
|
||||
sensitive: bool = False
|
||||
type: str = 'Note'
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ class OrderedCollection(ActivityObject):
|
|||
first: str
|
||||
last: str = ''
|
||||
name: str = ''
|
||||
owner: str = ''
|
||||
type: str = 'OrderedCollection'
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,17 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Dict
|
||||
|
||||
from .base_activity import ActivityObject, Image, PublicKey
|
||||
from .base_activity import ActivityObject
|
||||
from .image import Image
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class PublicKey(ActivityObject):
|
||||
''' public key block '''
|
||||
owner: str
|
||||
publicKeyPem: str
|
||||
type: str = 'PublicKey'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Person(ActivityObject):
|
||||
|
@ -15,8 +25,8 @@ class Person(ActivityObject):
|
|||
summary: str
|
||||
publicKey: PublicKey
|
||||
endpoints: Dict
|
||||
icon: Image = field(default=lambda: {})
|
||||
bookwyrmUser: str = False
|
||||
icon: Image = field(default_factory=lambda: {})
|
||||
bookwyrmUser: bool = False
|
||||
manuallyApprovesFollowers: str = False
|
||||
discoverable: str = True
|
||||
type: str = 'Person'
|
||||
|
|
|
@ -3,6 +3,7 @@ from dataclasses import dataclass
|
|||
from typing import List
|
||||
|
||||
from .base_activity import ActivityObject, Signature
|
||||
from .book import Book
|
||||
|
||||
@dataclass(init=False)
|
||||
class Verb(ActivityObject):
|
||||
|
@ -21,6 +22,14 @@ class Create(Verb):
|
|||
type: str = 'Create'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Delete(Verb):
|
||||
''' Create activity '''
|
||||
to: List
|
||||
cc: List
|
||||
type: str = 'Delete'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Update(Verb):
|
||||
''' Update activity '''
|
||||
|
@ -61,6 +70,13 @@ class Add(Verb):
|
|||
type: str = 'Add'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class AddBook(Verb):
|
||||
'''Add activity that's aware of the book obj '''
|
||||
target: Book
|
||||
type: str = 'Add'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Remove(Verb):
|
||||
'''Remove activity '''
|
||||
|
|
|
@ -4,3 +4,5 @@ from bookwyrm import models
|
|||
|
||||
admin.site.register(models.SiteSettings)
|
||||
admin.site.register(models.User)
|
||||
admin.site.register(models.FederatedServer)
|
||||
admin.site.register(models.Connector)
|
||||
|
|
|
@ -16,23 +16,6 @@ def get_edition(book_id):
|
|||
return book
|
||||
|
||||
|
||||
def get_or_create_book(remote_id):
|
||||
''' pull up a book record by whatever means possible '''
|
||||
book = models.Book.objects.select_subclasses().filter(
|
||||
remote_id=remote_id
|
||||
).first()
|
||||
if book:
|
||||
return book
|
||||
|
||||
connector = get_or_create_connector(remote_id)
|
||||
|
||||
# raises ConnectorException
|
||||
book = connector.get_or_create_book(remote_id)
|
||||
if book:
|
||||
load_more_data.delay(book.id)
|
||||
return book
|
||||
|
||||
|
||||
def get_or_create_connector(remote_id):
|
||||
''' get the connector related to the author's server '''
|
||||
url = urlparse(remote_id)
|
||||
|
@ -50,7 +33,7 @@ def get_or_create_connector(remote_id):
|
|||
books_url='https://%s/book' % identifier,
|
||||
covers_url='https://%s/images/covers' % identifier,
|
||||
search_url='https://%s/search?q=' % identifier,
|
||||
priority=3
|
||||
priority=2
|
||||
)
|
||||
|
||||
return load_connector(connector_info)
|
||||
|
@ -64,14 +47,14 @@ def load_more_data(book_id):
|
|||
connector.expand_book_data(book)
|
||||
|
||||
|
||||
def search(query):
|
||||
def search(query, min_confidence=0.1):
|
||||
''' find books based on arbitary keywords '''
|
||||
results = []
|
||||
dedup_slug = lambda r: '%s/%s/%s' % (r.title, r.author, r.year)
|
||||
result_index = set()
|
||||
for connector in get_connectors():
|
||||
try:
|
||||
result_set = connector.search(query)
|
||||
result_set = connector.search(query, min_confidence=min_confidence)
|
||||
except HTTPError:
|
||||
continue
|
||||
|
||||
|
@ -87,27 +70,21 @@ def search(query):
|
|||
return results
|
||||
|
||||
|
||||
def local_search(query):
|
||||
def local_search(query, min_confidence=0.1):
|
||||
''' only look at local search results '''
|
||||
connector = load_connector(models.Connector.objects.get(local=True))
|
||||
return connector.search(query)
|
||||
return connector.search(query, min_confidence=min_confidence)
|
||||
|
||||
|
||||
def first_search_result(query):
|
||||
def first_search_result(query, min_confidence=0.1):
|
||||
''' search until you find a result that fits '''
|
||||
for connector in get_connectors():
|
||||
result = connector.search(query)
|
||||
result = connector.search(query, min_confidence=min_confidence)
|
||||
if result:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
|
||||
def update_book(book, data=None):
|
||||
''' re-sync with the original data source '''
|
||||
connector = load_connector(book.connector)
|
||||
connector.update_book(book, data=data)
|
||||
|
||||
|
||||
def get_connectors():
|
||||
''' load all connectors '''
|
||||
for info in models.Connector.objects.order_by('priority').all():
|
||||
|
|
|
@ -13,7 +13,6 @@ def get_public_recipients(user, software=None):
|
|||
''' everybody and their public inboxes '''
|
||||
followers = user.followers.filter(local=False)
|
||||
if software:
|
||||
# TODO: eventually we may want to handle particular software differently
|
||||
followers = followers.filter(bookwyrm_user=(software == 'bookwyrm'))
|
||||
|
||||
# we want shared inboxes when available
|
||||
|
@ -36,7 +35,6 @@ def broadcast(sender, activity, software=None, \
|
|||
# start with parsing the direct recipients
|
||||
recipients = [u.inbox for u in direct_recipients or []]
|
||||
# and then add any other recipients
|
||||
# TODO: other kinds of privacy
|
||||
if privacy == 'public':
|
||||
recipients += get_public_recipients(sender, software=software)
|
||||
broadcast_task.delay(
|
||||
|
@ -55,7 +53,6 @@ def broadcast_task(sender_id, activity, recipients):
|
|||
try:
|
||||
sign_and_send(sender, activity, recipient)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
# TODO: maybe keep track of users who cause errors
|
||||
errors.append({
|
||||
'error': str(e),
|
||||
'recipient': recipient,
|
||||
|
@ -64,15 +61,14 @@ def broadcast_task(sender_id, activity, recipients):
|
|||
return errors
|
||||
|
||||
|
||||
def sign_and_send(sender, activity, destination):
|
||||
def sign_and_send(sender, data, destination):
|
||||
''' crpyto whatever and http junk '''
|
||||
now = http_date()
|
||||
|
||||
if not sender.private_key:
|
||||
if not sender.key_pair.private_key:
|
||||
# this shouldn't happen. it would be bad if it happened.
|
||||
raise ValueError('No private key found for sender')
|
||||
|
||||
data = json.dumps(activity).encode('utf-8')
|
||||
digest = make_digest(data)
|
||||
|
||||
response = requests.post(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
''' bring connectors into the namespace '''
|
||||
from .settings import CONNECTORS
|
||||
from .abstract_connector import ConnectorException
|
||||
from .abstract_connector import get_data, get_image
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
''' functionality outline for a book data connector '''
|
||||
from abc import ABC, abstractmethod
|
||||
from dateutil import parser
|
||||
from dataclasses import dataclass
|
||||
import pytz
|
||||
import requests
|
||||
from urllib3.exceptions import RequestError
|
||||
|
||||
from django.db import transaction
|
||||
from dateutil import parser
|
||||
import requests
|
||||
from requests import HTTPError
|
||||
from requests.exceptions import SSLError
|
||||
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
class ConnectorException(Exception):
|
||||
class ConnectorException(HTTPError):
|
||||
''' when the connector can't do what was asked '''
|
||||
|
||||
|
||||
class AbstractConnector(ABC):
|
||||
''' generic book data connector '''
|
||||
|
||||
class AbstractMinimalConnector(ABC):
|
||||
''' just the bare bones, for other bookwyrm instances '''
|
||||
def __init__(self, identifier):
|
||||
# load connector settings
|
||||
info = models.Connector.objects.get(identifier=identifier)
|
||||
self.connector = info
|
||||
|
||||
self.key_mappings = []
|
||||
|
||||
# fields we want to look for in book data to copy over
|
||||
# title we handle separately.
|
||||
self.book_mappings = []
|
||||
|
||||
# the things in the connector model to copy over
|
||||
self_fields = [
|
||||
'base_url',
|
||||
|
@ -41,16 +38,7 @@ class AbstractConnector(ABC):
|
|||
for field in self_fields:
|
||||
setattr(self, field, getattr(info, field))
|
||||
|
||||
|
||||
def is_available(self):
|
||||
''' check if you're allowed to use this connector '''
|
||||
if self.max_query_count is not None:
|
||||
if self.connector.query_count >= self.max_query_count:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def search(self, query):
|
||||
def search(self, query, min_confidence=None):
|
||||
''' free text search '''
|
||||
resp = requests.get(
|
||||
'%s%s' % (self.search_url, query),
|
||||
|
@ -67,12 +55,43 @@ class AbstractConnector(ABC):
|
|||
results.append(self.format_search_result(doc))
|
||||
return results
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_or_create_book(self, remote_id):
|
||||
''' pull up a book record by whatever means possible '''
|
||||
|
||||
@abstractmethod
|
||||
def parse_search_data(self, data):
|
||||
''' turn the result json from a search into a list '''
|
||||
|
||||
@abstractmethod
|
||||
def format_search_result(self, search_result):
|
||||
''' create a SearchResult obj from json '''
|
||||
|
||||
|
||||
class AbstractConnector(AbstractMinimalConnector):
|
||||
''' generic book data connector '''
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
self.key_mappings = []
|
||||
|
||||
# fields we want to look for in book data to copy over
|
||||
# title we handle separately.
|
||||
self.book_mappings = []
|
||||
|
||||
|
||||
def is_available(self):
|
||||
''' check if you're allowed to use this connector '''
|
||||
if self.max_query_count is not None:
|
||||
if self.connector.query_count >= self.max_query_count:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_or_create_book(self, remote_id):
|
||||
# try to load the book
|
||||
book = models.Book.objects.select_subclasses().filter(
|
||||
remote_id=remote_id
|
||||
origin_id=remote_id
|
||||
).first()
|
||||
if book:
|
||||
if isinstance(book, models.Work):
|
||||
|
@ -112,22 +131,26 @@ class AbstractConnector(ABC):
|
|||
# remember this hack: re-use the work data as the edition data
|
||||
work_data = data
|
||||
|
||||
if not work_data or not edition_data:
|
||||
raise ConnectorException('Unable to load book data: %s' % remote_id)
|
||||
|
||||
# at this point, we need to figure out the work, edition, or both
|
||||
# atomic so that we don't save a work with no edition for vice versa
|
||||
with transaction.atomic():
|
||||
if not work:
|
||||
work_key = work_data.get('url')
|
||||
work_key = self.get_remote_id_from_data(work_data)
|
||||
work = self.create_book(work_key, work_data, models.Work)
|
||||
|
||||
if not edition:
|
||||
ed_key = edition_data.get('url')
|
||||
ed_key = self.get_remote_id_from_data(edition_data)
|
||||
edition = self.create_book(ed_key, edition_data, models.Edition)
|
||||
edition.default = True
|
||||
edition.parent_work = work
|
||||
edition.save()
|
||||
work.default_edition = edition
|
||||
work.save()
|
||||
|
||||
# now's our change to fill in author gaps
|
||||
if not edition.authors and work.authors:
|
||||
if not edition.authors.exists() and work.authors.exists():
|
||||
edition.authors.set(work.authors.all())
|
||||
edition.author_text = work.author_text
|
||||
edition.save()
|
||||
|
@ -141,7 +164,7 @@ class AbstractConnector(ABC):
|
|||
def create_book(self, remote_id, data, model):
|
||||
''' create a work or edition from data '''
|
||||
book = model.objects.create(
|
||||
remote_id=remote_id,
|
||||
origin_id=remote_id,
|
||||
title=data['title'],
|
||||
connector=self.connector,
|
||||
)
|
||||
|
@ -152,9 +175,11 @@ class AbstractConnector(ABC):
|
|||
''' for creating a new book or syncing with data '''
|
||||
book = update_from_mappings(book, data, self.book_mappings)
|
||||
|
||||
author_text = []
|
||||
for author in self.get_authors_from_data(data):
|
||||
book.authors.add(author)
|
||||
book.author_text = ', '.join(a.display_name for a in book.authors.all())
|
||||
author_text.append(author.name)
|
||||
book.author_text = ', '.join(author_text)
|
||||
book.save()
|
||||
|
||||
if not update_cover:
|
||||
|
@ -207,6 +232,11 @@ class AbstractConnector(ABC):
|
|||
return None
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_remote_id_from_data(self, data):
|
||||
''' otherwise we won't properly set the remote_id in the db '''
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def is_work_data(self, data):
|
||||
''' differentiate works and editions '''
|
||||
|
@ -231,17 +261,6 @@ class AbstractConnector(ABC):
|
|||
def get_cover_from_data(self, data):
|
||||
''' load cover '''
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def parse_search_data(self, data):
|
||||
''' turn the result json from a search into a list '''
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def format_search_result(self, search_result):
|
||||
''' create a SearchResult obj from json '''
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def expand_book_data(self, book):
|
||||
''' get more info on a book '''
|
||||
|
@ -284,25 +303,44 @@ def get_date(date_string):
|
|||
|
||||
def get_data(url):
|
||||
''' wrapper for request.get '''
|
||||
resp = requests.get(
|
||||
url,
|
||||
headers={
|
||||
'Accept': 'application/json; charset=utf-8',
|
||||
},
|
||||
)
|
||||
try:
|
||||
resp = requests.get(
|
||||
url,
|
||||
headers={
|
||||
'Accept': 'application/json; charset=utf-8',
|
||||
},
|
||||
)
|
||||
except RequestError:
|
||||
raise ConnectorException()
|
||||
if not resp.ok:
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError:
|
||||
raise ConnectorException()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_image(url):
|
||||
''' wrapper for requesting an image '''
|
||||
try:
|
||||
resp = requests.get(url)
|
||||
except (RequestError, SSLError):
|
||||
return None
|
||||
if not resp.ok:
|
||||
return None
|
||||
return resp
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchResult:
|
||||
''' standardized search result object '''
|
||||
def __init__(self, title, key, author, year):
|
||||
self.title = title
|
||||
self.key = key
|
||||
self.author = author
|
||||
self.year = year
|
||||
title: str
|
||||
key: str
|
||||
author: str
|
||||
year: str
|
||||
confidence: int = 1
|
||||
|
||||
def __repr__(self):
|
||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||
|
|
|
@ -1,108 +1,16 @@
|
|||
''' using another bookwyrm instance as a source of book data '''
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.files.base import ContentFile
|
||||
import requests
|
||||
|
||||
from bookwyrm import models
|
||||
from .abstract_connector import AbstractConnector, SearchResult, Mapping
|
||||
from .abstract_connector import update_from_mappings, get_date, get_data
|
||||
from bookwyrm import activitypub, models
|
||||
from .abstract_connector import AbstractMinimalConnector, SearchResult
|
||||
|
||||
|
||||
class Connector(AbstractConnector):
|
||||
''' interact with other instances '''
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
self.key_mappings = [
|
||||
Mapping('isbn_13', model=models.Edition),
|
||||
Mapping('isbn_10', model=models.Edition),
|
||||
Mapping('lccn', model=models.Work),
|
||||
Mapping('oclc_number', model=models.Edition),
|
||||
Mapping('openlibrary_key'),
|
||||
Mapping('goodreads_key'),
|
||||
Mapping('asin'),
|
||||
]
|
||||
|
||||
self.book_mappings = self.key_mappings + [
|
||||
Mapping('sort_title'),
|
||||
Mapping('subtitle'),
|
||||
Mapping('description'),
|
||||
Mapping('languages'),
|
||||
Mapping('series'),
|
||||
Mapping('series_number'),
|
||||
Mapping('subjects'),
|
||||
Mapping('subject_places'),
|
||||
Mapping('first_published_date'),
|
||||
Mapping('published_date'),
|
||||
Mapping('pages'),
|
||||
Mapping('physical_format'),
|
||||
Mapping('publishers'),
|
||||
]
|
||||
|
||||
self.author_mappings = [
|
||||
Mapping('born', remote_field='birth_date', formatter=get_date),
|
||||
Mapping('died', remote_field='death_date', formatter=get_date),
|
||||
Mapping('bio'),
|
||||
]
|
||||
|
||||
|
||||
def is_work_data(self, data):
|
||||
return data['book_type'] == 'Work'
|
||||
|
||||
|
||||
def get_edition_from_work_data(self, data):
|
||||
return data['editions'][0]
|
||||
|
||||
|
||||
def get_work_from_edition_date(self, data):
|
||||
return data['work']
|
||||
|
||||
|
||||
def get_authors_from_data(self, data):
|
||||
for author_url in data.get('authors', []):
|
||||
yield self.get_or_create_author(author_url)
|
||||
|
||||
|
||||
def get_cover_from_data(self, data):
|
||||
cover_data = data.get('attachment')
|
||||
if not cover_data:
|
||||
return None
|
||||
cover_url = cover_data[0].get('url')
|
||||
response = requests.get(cover_url)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
|
||||
image_name = str(uuid4()) + cover_url.split('.')[-1]
|
||||
image_content = ContentFile(response.content)
|
||||
return [image_name, image_content]
|
||||
|
||||
|
||||
def get_or_create_author(self, remote_id):
|
||||
''' load that author '''
|
||||
try:
|
||||
return models.Author.objects.get(remote_id=remote_id)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
data = get_data(remote_id)
|
||||
|
||||
# ingest a new author
|
||||
author = models.Author(remote_id=remote_id)
|
||||
author = update_from_mappings(author, data, self.author_mappings)
|
||||
author.save()
|
||||
|
||||
return author
|
||||
class Connector(AbstractMinimalConnector):
|
||||
''' this is basically just for search '''
|
||||
|
||||
def get_or_create_book(self, remote_id):
|
||||
return activitypub.resolve_remote_id(models.Edition, remote_id)
|
||||
|
||||
def parse_search_data(self, data):
|
||||
return data
|
||||
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
return SearchResult(**search_result)
|
||||
|
||||
|
||||
def expand_book_data(self, book):
|
||||
# TODO
|
||||
pass
|
||||
|
|
|
@ -7,8 +7,7 @@ from django.core.files.base import ContentFile
|
|||
from bookwyrm import models
|
||||
from .abstract_connector import AbstractConnector, SearchResult, Mapping
|
||||
from .abstract_connector import ConnectorException
|
||||
from .abstract_connector import update_from_mappings
|
||||
from .abstract_connector import get_date, get_data
|
||||
from .abstract_connector import get_date, get_data, update_from_mappings
|
||||
from .openlibrary_languages import languages
|
||||
|
||||
|
||||
|
@ -66,12 +65,20 @@ class Connector(AbstractConnector):
|
|||
]
|
||||
|
||||
self.author_mappings = [
|
||||
Mapping('name'),
|
||||
Mapping('born', remote_field='birth_date', formatter=get_date),
|
||||
Mapping('died', remote_field='death_date', formatter=get_date),
|
||||
Mapping('bio', formatter=get_description),
|
||||
]
|
||||
|
||||
|
||||
def get_remote_id_from_data(self, data):
|
||||
try:
|
||||
key = data['key']
|
||||
except KeyError:
|
||||
raise ConnectorException('Invalid book data')
|
||||
return '%s/%s' % (self.books_url, key)
|
||||
|
||||
|
||||
def is_work_data(self, data):
|
||||
return bool(re.match(r'^[\/\w]+OL\d+W$', data['key']))
|
||||
|
@ -129,10 +136,10 @@ class Connector(AbstractConnector):
|
|||
key = self.books_url + search_result['key']
|
||||
author = search_result.get('author_name') or ['Unknown']
|
||||
return SearchResult(
|
||||
search_result.get('title'),
|
||||
key,
|
||||
', '.join(author),
|
||||
search_result.get('first_publish_year'),
|
||||
title=search_result.get('title'),
|
||||
key=key,
|
||||
author=', '.join(author),
|
||||
year=search_result.get('first_publish_year'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -170,21 +177,15 @@ class Connector(AbstractConnector):
|
|||
''' load that author '''
|
||||
if not re.match(r'^OL\d+A$', olkey):
|
||||
raise ValueError('Invalid OpenLibrary author ID')
|
||||
try:
|
||||
return models.Author.objects.get(openlibrary_key=olkey)
|
||||
except models.Author.DoesNotExist:
|
||||
pass
|
||||
author = models.Author.objects.filter(openlibrary_key=olkey).first()
|
||||
if author:
|
||||
return author
|
||||
|
||||
url = '%s/authors/%s.json' % (self.base_url, olkey)
|
||||
data = get_data(url)
|
||||
|
||||
author = models.Author(openlibrary_key=olkey)
|
||||
author = update_from_mappings(author, data, self.author_mappings)
|
||||
name = data.get('name')
|
||||
# TODO this is making some BOLD assumption
|
||||
if name:
|
||||
author.last_name = name.split(' ')[-1]
|
||||
author.first_name = ' '.join(name.split(' ')[:-1])
|
||||
author.save()
|
||||
|
||||
return author
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
''' using a bookwyrm instance as a source of book data '''
|
||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||
from django.db.models import F
|
||||
|
||||
from bookwyrm import models
|
||||
from .abstract_connector import AbstractConnector, SearchResult
|
||||
|
@ -7,30 +8,33 @@ from .abstract_connector import AbstractConnector, SearchResult
|
|||
|
||||
class Connector(AbstractConnector):
|
||||
''' instantiate a connector '''
|
||||
def search(self, query):
|
||||
def search(self, query, min_confidence=0.1):
|
||||
''' right now you can't search bookwyrm sorry, but when
|
||||
that gets implemented it will totally rule '''
|
||||
vector = SearchVector('title', weight='A') +\
|
||||
SearchVector('subtitle', weight='B') +\
|
||||
SearchVector('author_text', weight='A') +\
|
||||
SearchVector('author_text', weight='C') +\
|
||||
SearchVector('isbn_13', weight='A') +\
|
||||
SearchVector('isbn_10', weight='A') +\
|
||||
SearchVector('openlibrary_key', weight='B') +\
|
||||
SearchVector('goodreads_key', weight='B') +\
|
||||
SearchVector('asin', weight='B') +\
|
||||
SearchVector('oclc_number', weight='B') +\
|
||||
SearchVector('remote_id', weight='B') +\
|
||||
SearchVector('description', weight='C') +\
|
||||
SearchVector('series', weight='C')
|
||||
SearchVector('openlibrary_key', weight='C') +\
|
||||
SearchVector('goodreads_key', weight='C') +\
|
||||
SearchVector('asin', weight='C') +\
|
||||
SearchVector('oclc_number', weight='C') +\
|
||||
SearchVector('remote_id', weight='C') +\
|
||||
SearchVector('description', weight='D') +\
|
||||
SearchVector('series', weight='D')
|
||||
|
||||
results = models.Edition.objects.annotate(
|
||||
search=vector
|
||||
).annotate(
|
||||
rank=SearchRank(vector, query)
|
||||
).filter(
|
||||
rank__gt=0
|
||||
rank__gt=min_confidence
|
||||
).order_by('-rank')
|
||||
results = results.filter(default=True) or results
|
||||
|
||||
# remove non-default editions, if possible
|
||||
results = results.filter(parent_work__default_edition__id=F('id')) \
|
||||
or results
|
||||
|
||||
search_results = []
|
||||
for book in results[:10]:
|
||||
|
@ -42,14 +46,18 @@ class Connector(AbstractConnector):
|
|||
|
||||
def format_search_result(self, search_result):
|
||||
return SearchResult(
|
||||
search_result.title,
|
||||
search_result.local_id,
|
||||
search_result.author_text,
|
||||
search_result.published_date.year if \
|
||||
title=search_result.title,
|
||||
key=search_result.remote_id,
|
||||
author=search_result.author_text,
|
||||
year=search_result.published_date.year if \
|
||||
search_result.published_date else None,
|
||||
confidence=search_result.rank,
|
||||
)
|
||||
|
||||
|
||||
def get_remote_id_from_data(self, data):
|
||||
pass
|
||||
|
||||
def is_work_data(self, data):
|
||||
pass
|
||||
|
||||
|
|
8
bookwyrm/context_processors.py
Normal file
8
bookwyrm/context_processors.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
''' customize the info available in context for rendering templates '''
|
||||
from bookwyrm import models
|
||||
|
||||
def site_settings(request):
|
||||
''' include the custom info about the site '''
|
||||
return {
|
||||
'site': models.SiteSettings.objects.get()
|
||||
}
|
|
@ -6,7 +6,6 @@ from bookwyrm.tasks import app
|
|||
|
||||
def password_reset_email(reset_code):
|
||||
''' generate a password reset email '''
|
||||
# TODO; this should be tempalted
|
||||
site = models.SiteSettings.get()
|
||||
send_email.delay(
|
||||
reset_code.user.email,
|
||||
|
|
|
@ -5,6 +5,7 @@ from collections import defaultdict
|
|||
from django import forms
|
||||
from django.forms import ModelForm, PasswordInput, widgets
|
||||
from django.forms.widgets import Textarea
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import models
|
||||
|
||||
|
@ -29,6 +30,7 @@ class CustomForm(ModelForm):
|
|||
visible.field.widget.attrs['rows'] = None
|
||||
visible.field.widget.attrs['class'] = css_classes[input_type]
|
||||
|
||||
|
||||
class LoginForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
|
@ -52,47 +54,31 @@ class RegisterForm(CustomForm):
|
|||
class RatingForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Review
|
||||
fields = ['rating']
|
||||
fields = ['user', 'book', 'content', 'rating', 'privacy']
|
||||
|
||||
|
||||
class ReviewForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Review
|
||||
fields = ['name', 'content']
|
||||
help_texts = {f: None for f in fields}
|
||||
labels = {
|
||||
'name': 'Title',
|
||||
'content': 'Review',
|
||||
}
|
||||
fields = ['user', 'book', 'name', 'content', 'rating', 'privacy']
|
||||
|
||||
|
||||
class CommentForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Comment
|
||||
fields = ['content']
|
||||
help_texts = {f: None for f in fields}
|
||||
labels = {
|
||||
'content': 'Comment',
|
||||
}
|
||||
fields = ['user', 'book', 'content', 'privacy']
|
||||
|
||||
|
||||
class QuotationForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Quotation
|
||||
fields = ['quote', 'content']
|
||||
help_texts = {f: None for f in fields}
|
||||
labels = {
|
||||
'quote': 'Quote',
|
||||
'content': 'Comment',
|
||||
}
|
||||
fields = ['user', 'book', 'quote', 'content', 'privacy']
|
||||
|
||||
|
||||
class ReplyForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = ['content']
|
||||
help_texts = {f: None for f in fields}
|
||||
labels = {'content': 'Comment'}
|
||||
fields = ['user', 'content', 'reply_parent', 'privacy']
|
||||
|
||||
|
||||
class EditUserForm(CustomForm):
|
||||
|
@ -158,7 +144,7 @@ class ExpiryWidget(widgets.Select):
|
|||
else:
|
||||
return selected_string # "This will raise
|
||||
|
||||
return datetime.datetime.now() + interval
|
||||
return timezone.now() + interval
|
||||
|
||||
class CreateInviteForm(CustomForm):
|
||||
class Meta:
|
||||
|
@ -174,3 +160,8 @@ class CreateInviteForm(CustomForm):
|
|||
choices=[(i, "%d uses" % (i,)) for i in [1, 5, 10, 25, 50, 100]]
|
||||
+ [(None, 'Unlimited')])
|
||||
}
|
||||
|
||||
class ShelfForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Shelf
|
||||
fields = ['user', 'name', 'privacy']
|
||||
|
|
|
@ -1,25 +1,41 @@
|
|||
''' handle reading a csv from goodreads '''
|
||||
import csv
|
||||
from requests import HTTPError
|
||||
import logging
|
||||
|
||||
from bookwyrm import outgoing
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.models import ImportJob, ImportItem
|
||||
from bookwyrm.status import create_notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# TODO: remove or increase once we're confident it's not causing problems.
|
||||
MAX_ENTRIES = 500
|
||||
|
||||
|
||||
def create_job(user, csv_file):
|
||||
def create_job(user, csv_file, include_reviews, privacy):
|
||||
''' check over a csv and creates a database entry for the job'''
|
||||
job = ImportJob.objects.create(user=user)
|
||||
job = ImportJob.objects.create(
|
||||
user=user,
|
||||
include_reviews=include_reviews,
|
||||
privacy=privacy
|
||||
)
|
||||
for index, entry in enumerate(list(csv.DictReader(csv_file))[:MAX_ENTRIES]):
|
||||
if not all(x in entry for x in ('ISBN13', 'Title', 'Author')):
|
||||
raise ValueError("Author, title, and isbn must be in data.")
|
||||
raise ValueError('Author, title, and isbn must be in data.')
|
||||
ImportItem(job=job, index=index, data=entry).save()
|
||||
return job
|
||||
|
||||
def create_retry_job(user, original_job, items):
|
||||
''' retry items that didn't import '''
|
||||
job = ImportJob.objects.create(
|
||||
user=user,
|
||||
include_reviews=original_job.include_reviews,
|
||||
privacy=original_job.privacy,
|
||||
retry=True
|
||||
)
|
||||
for item in items:
|
||||
ImportItem(job=job, index=item.index, data=item.data).save()
|
||||
return job
|
||||
|
||||
def start_import(job):
|
||||
''' initalizes a csv import job '''
|
||||
|
@ -37,18 +53,21 @@ def import_data(job_id):
|
|||
for item in job.items.all():
|
||||
try:
|
||||
item.resolve()
|
||||
except HTTPError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
item.fail_reason = 'Error loading book'
|
||||
item.save()
|
||||
continue
|
||||
|
||||
if item.book:
|
||||
item.save()
|
||||
results.append(item)
|
||||
else:
|
||||
item.fail_reason = "Could not match book on OpenLibrary"
|
||||
item.save()
|
||||
|
||||
status = outgoing.handle_import_books(job.user, results)
|
||||
if status:
|
||||
job.import_status = status
|
||||
job.save()
|
||||
# shelves book and handles reviews
|
||||
outgoing.handle_imported_book(
|
||||
job.user, item, job.include_reviews, job.privacy)
|
||||
else:
|
||||
item.fail_reason = 'Could not find a match for book'
|
||||
item.save()
|
||||
finally:
|
||||
create_notification(job.user, 'IMPORT', related_import=job)
|
||||
|
|
|
@ -8,9 +8,8 @@ from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
|||
from django.views.decorators.csrf import csrf_exempt
|
||||
import requests
|
||||
|
||||
from bookwyrm import activitypub, books_manager, models, outgoing
|
||||
from bookwyrm import activitypub, models, outgoing
|
||||
from bookwyrm import status as status_builder
|
||||
from bookwyrm.remote_user import get_or_create_remote_user, refresh_remote_user
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.signatures import Signature
|
||||
|
||||
|
@ -32,12 +31,14 @@ def inbox(request, username):
|
|||
@csrf_exempt
|
||||
def shared_inbox(request):
|
||||
''' incoming activitypub events '''
|
||||
# TODO: should this be functionally different from the non-shared inbox??
|
||||
if request.method == 'GET':
|
||||
return HttpResponseNotFound()
|
||||
|
||||
try:
|
||||
activity = json.loads(request.body)
|
||||
resp = request.body
|
||||
activity = json.loads(resp)
|
||||
if isinstance(activity, str):
|
||||
activity = json.loads(activity)
|
||||
activity_object = activity['object']
|
||||
except (json.decoder.JSONDecodeError, KeyError):
|
||||
return HttpResponseBadRequest()
|
||||
|
@ -54,18 +55,22 @@ def shared_inbox(request):
|
|||
'Accept': handle_follow_accept,
|
||||
'Reject': handle_follow_reject,
|
||||
'Create': handle_create,
|
||||
'Delete': handle_delete_status,
|
||||
'Like': handle_favorite,
|
||||
'Announce': handle_boost,
|
||||
'Add': {
|
||||
'Tag': handle_tag,
|
||||
'Edition': handle_add,
|
||||
'Work': handle_add,
|
||||
},
|
||||
'Undo': {
|
||||
'Follow': handle_unfollow,
|
||||
'Like': handle_unfavorite,
|
||||
'Announce': handle_unboost,
|
||||
},
|
||||
'Update': {
|
||||
'Person': None,# TODO: handle_update_user
|
||||
'Document': handle_update_book,
|
||||
'Person': handle_update_user,
|
||||
'Edition': handle_update_book,
|
||||
'Work': handle_update_book,
|
||||
},
|
||||
}
|
||||
activity_type = activity['type']
|
||||
|
@ -90,16 +95,20 @@ def has_valid_signature(request, activity):
|
|||
if key_actor != activity.get('actor'):
|
||||
raise ValueError("Wrong actor created signature.")
|
||||
|
||||
remote_user = get_or_create_remote_user(key_actor)
|
||||
remote_user = activitypub.resolve_remote_id(models.User, key_actor)
|
||||
if not remote_user:
|
||||
return False
|
||||
|
||||
try:
|
||||
signature.verify(remote_user.public_key, request)
|
||||
signature.verify(remote_user.key_pair.public_key, request)
|
||||
except ValueError:
|
||||
old_key = remote_user.public_key
|
||||
refresh_remote_user(remote_user)
|
||||
if remote_user.public_key == old_key:
|
||||
old_key = remote_user.key_pair.public_key
|
||||
remote_user = activitypub.resolve_remote_id(
|
||||
models.User, remote_user.remote_id, refresh=True
|
||||
)
|
||||
if remote_user.key_pair.public_key == old_key:
|
||||
raise # Key unchanged.
|
||||
signature.verify(remote_user.public_key, request)
|
||||
signature.verify(remote_user.key_pair.public_key, request)
|
||||
except (ValueError, requests.exceptions.HTTPError):
|
||||
return False
|
||||
return True
|
||||
|
@ -108,47 +117,34 @@ def has_valid_signature(request, activity):
|
|||
@app.task
|
||||
def handle_follow(activity):
|
||||
''' someone wants to follow a local user '''
|
||||
# figure out who they want to follow -- not using get_or_create because
|
||||
# we only allow you to follow local users
|
||||
to_follow = models.User.objects.get(remote_id=activity['object'])
|
||||
# raises models.User.DoesNotExist id the remote id is not found
|
||||
|
||||
# figure out who the actor is
|
||||
user = get_or_create_remote_user(activity['actor'])
|
||||
try:
|
||||
relationship = models.UserFollowRequest.objects.create(
|
||||
user_subject=user,
|
||||
user_object=to_follow,
|
||||
relationship_id=activity['id']
|
||||
)
|
||||
relationship = activitypub.Follow(
|
||||
**activity
|
||||
).to_model(models.UserFollowRequest)
|
||||
except django.db.utils.IntegrityError as err:
|
||||
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
|
||||
raise
|
||||
# Duplicate follow request. Not sure what the correct behaviour is, but
|
||||
# just dropping it works for now. We should perhaps generate the
|
||||
# Accept, but then do we need to match the activity id?
|
||||
return
|
||||
relationship = models.UserFollowRequest.objects.get(
|
||||
remote_id=activity['id']
|
||||
)
|
||||
# send the accept normally for a duplicate request
|
||||
|
||||
if not to_follow.manually_approves_followers:
|
||||
status_builder.create_notification(
|
||||
to_follow,
|
||||
'FOLLOW',
|
||||
related_user=user
|
||||
)
|
||||
outgoing.handle_accept(user, to_follow, relationship)
|
||||
else:
|
||||
status_builder.create_notification(
|
||||
to_follow,
|
||||
'FOLLOW_REQUEST',
|
||||
related_user=user
|
||||
)
|
||||
manually_approves = relationship.user_object.manually_approves_followers
|
||||
|
||||
status_builder.create_notification(
|
||||
relationship.user_object,
|
||||
'FOLLOW_REQUEST' if manually_approves else 'FOLLOW',
|
||||
related_user=relationship.user_subject
|
||||
)
|
||||
if not manually_approves:
|
||||
outgoing.handle_accept(relationship)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_unfollow(activity):
|
||||
''' unfollow a local user '''
|
||||
obj = activity['object']
|
||||
requester = get_or_create_remote_user(obj['actor'])
|
||||
requester = activitypub.resolve_remote_id(models.user, obj['actor'])
|
||||
to_unfollow = models.User.objects.get(remote_id=obj['object'])
|
||||
# raises models.User.DoesNotExist
|
||||
|
||||
|
@ -161,7 +157,7 @@ def handle_follow_accept(activity):
|
|||
# figure out who they want to follow
|
||||
requester = models.User.objects.get(remote_id=activity['object']['actor'])
|
||||
# figure out who they are
|
||||
accepter = get_or_create_remote_user(activity['actor'])
|
||||
accepter = activitypub.resolve_remote_id(models.User, activity['actor'])
|
||||
|
||||
try:
|
||||
request = models.UserFollowRequest.objects.get(
|
||||
|
@ -178,34 +174,32 @@ def handle_follow_accept(activity):
|
|||
def handle_follow_reject(activity):
|
||||
''' someone is rejecting a follow request '''
|
||||
requester = models.User.objects.get(remote_id=activity['object']['actor'])
|
||||
rejecter = get_or_create_remote_user(activity['actor'])
|
||||
rejecter = activitypub.resolve_remote_id(models.User, activity['actor'])
|
||||
|
||||
request = models.UserFollowRequest.objects.get(
|
||||
user_subject=requester,
|
||||
user_object=rejecter
|
||||
)
|
||||
request.delete()
|
||||
#raises models.UserFollowRequest.DoesNotExist:
|
||||
#raises models.UserFollowRequest.DoesNotExist
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_create(activity):
|
||||
''' someone did something, good on them '''
|
||||
if activity['object'].get('type') not in \
|
||||
['Note', 'Comment', 'Quotation', 'Review']:
|
||||
# if it's an article or unknown type, ignore it
|
||||
# deduplicate incoming activities
|
||||
status_id = activity['object']['id']
|
||||
if models.Status.objects.filter(remote_id=status_id).count():
|
||||
return
|
||||
|
||||
user = get_or_create_remote_user(activity['actor'])
|
||||
if user.local:
|
||||
# we really oughtn't even be sending in this case
|
||||
serializer = activitypub.activity_objects[activity['type']]
|
||||
status = serializer(**activity)
|
||||
try:
|
||||
model = models.activity_models[activity.type]
|
||||
except KeyError:
|
||||
# not a type of status we are prepared to deserialize
|
||||
return
|
||||
|
||||
# render the json into an activity object
|
||||
serializer = activitypub.activity_objects[activity['object']['type']]
|
||||
activity = serializer(**activity['object'])
|
||||
|
||||
# ignore notes that aren't replies to known statuses
|
||||
if activity.type == 'Note':
|
||||
reply = models.Status.objects.filter(
|
||||
remote_id=activity.inReplyTo
|
||||
|
@ -213,9 +207,7 @@ def handle_create(activity):
|
|||
if not reply:
|
||||
return
|
||||
|
||||
model = models.activity_models[activity.type]
|
||||
status = activity.to_model(model)
|
||||
|
||||
activity.to_model(model)
|
||||
# create a notification if this is a reply
|
||||
if status.reply_parent and status.reply_parent.user.local:
|
||||
status_builder.create_notification(
|
||||
|
@ -226,72 +218,105 @@ def handle_create(activity):
|
|||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_delete_status(activity):
|
||||
''' remove a status '''
|
||||
try:
|
||||
status_id = activity['object']['id']
|
||||
except TypeError:
|
||||
# this isn't a great fix, because you hit this when mastadon
|
||||
# is trying to delete a user.
|
||||
return
|
||||
try:
|
||||
status = models.Status.objects.select_subclasses().get(
|
||||
remote_id=status_id
|
||||
)
|
||||
except models.Status.DoesNotExist:
|
||||
return
|
||||
status_builder.delete_status(status)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_favorite(activity):
|
||||
''' approval of your good good post '''
|
||||
fav = activitypub.Like(**activity['object'])
|
||||
# raises ValueError in to_model if a foreign key could not be resolved in
|
||||
fav = activitypub.Like(**activity)
|
||||
|
||||
liker = get_or_create_remote_user(activity['actor'])
|
||||
if liker.local:
|
||||
fav = fav.to_model(models.Favorite)
|
||||
if fav.user.local:
|
||||
return
|
||||
|
||||
status = fav.to_model(models.Favorite)
|
||||
|
||||
status_builder.create_notification(
|
||||
status.user,
|
||||
fav.status.user,
|
||||
'FAVORITE',
|
||||
related_user=liker,
|
||||
related_status=status,
|
||||
related_user=fav.user,
|
||||
related_status=fav.status,
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_unfavorite(activity):
|
||||
''' approval of your good good post '''
|
||||
like = activitypub.Like(**activity['object'])
|
||||
fav = models.Favorite.objects.filter(remote_id=like.id).first()
|
||||
|
||||
fav.delete()
|
||||
like = models.Favorite.objects.filter(
|
||||
remote_id=activity['object']['id']
|
||||
).first()
|
||||
if not like:
|
||||
return
|
||||
like.delete()
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_boost(activity):
|
||||
''' someone gave us a boost! '''
|
||||
status_id = activity['object'].split('/')[-1]
|
||||
status = models.Status.objects.get(id=status_id)
|
||||
booster = get_or_create_remote_user(activity['actor'])
|
||||
try:
|
||||
boost = activitypub.Boost(**activity).to_model(models.Boost)
|
||||
except activitypub.ActivitySerializerError:
|
||||
# this probably just means we tried to boost an unknown status
|
||||
return
|
||||
|
||||
if not booster.local:
|
||||
status_builder.create_boost_from_activity(booster, activity)
|
||||
|
||||
status_builder.create_notification(
|
||||
status.user,
|
||||
'BOOST',
|
||||
related_user=booster,
|
||||
related_status=status,
|
||||
)
|
||||
if not boost.user.local:
|
||||
status_builder.create_notification(
|
||||
boost.boosted_status.user,
|
||||
'BOOST',
|
||||
related_user=boost.user,
|
||||
related_status=boost.boosted_status,
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_tag(activity):
|
||||
''' someone is tagging a book '''
|
||||
user = get_or_create_remote_user(activity['actor'])
|
||||
if not user.local:
|
||||
book = activity['target']['id']
|
||||
status_builder.create_tag(user, book, activity['object']['name'])
|
||||
def handle_unboost(activity):
|
||||
''' someone gave us a boost! '''
|
||||
boost = models.Boost.objects.filter(
|
||||
remote_id=activity['object']['id']
|
||||
).first()
|
||||
if boost:
|
||||
boost.delete()
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_add(activity):
|
||||
''' putting a book on a shelf '''
|
||||
#this is janky as heck but I haven't thought of a better solution
|
||||
try:
|
||||
activitypub.AddBook(**activity).to_model(models.ShelfBook)
|
||||
except activitypub.ActivitySerializerError:
|
||||
activitypub.AddBook(**activity).to_model(models.Tag)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_update_user(activity):
|
||||
''' receive an updated user Person activity object '''
|
||||
try:
|
||||
user = models.User.objects.get(remote_id=activity['object']['id'])
|
||||
except models.User.DoesNotExist:
|
||||
# who is this person? who cares
|
||||
return
|
||||
activitypub.Person(
|
||||
**activity['object']
|
||||
).to_model(models.User, instance=user)
|
||||
# model save() happens in the to_model function
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_update_book(activity):
|
||||
''' a remote instance changed a book (Document) '''
|
||||
document = activity['object']
|
||||
# check if we have their copy and care about their updates
|
||||
book = models.Book.objects.select_subclasses().filter(
|
||||
remote_id=document['url'],
|
||||
sync=True,
|
||||
).first()
|
||||
if not book:
|
||||
return
|
||||
|
||||
books_manager.update_book(book, data=document)
|
||||
activitypub.Edition(**activity['object']).to_model(models.Edition)
|
||||
|
|
104
bookwyrm/management/commands/initdb.py
Normal file
104
bookwyrm/management/commands/initdb.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from bookwyrm.models import Connector, SiteSettings, User
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
def init_groups():
|
||||
groups = ['admin', 'moderator', 'editor']
|
||||
for group in groups:
|
||||
Group.objects.create(name=group)
|
||||
|
||||
def init_permissions():
|
||||
permissions = [{
|
||||
'codename': 'edit_instance_settings',
|
||||
'name': 'change the instance info',
|
||||
'groups': ['admin',]
|
||||
}, {
|
||||
'codename': 'set_user_group',
|
||||
'name': 'change what group a user is in',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'control_federation',
|
||||
'name': 'control who to federate with',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'create_invites',
|
||||
'name': 'issue invitations to join',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'moderate_user',
|
||||
'name': 'deactivate or silence a user',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'moderate_post',
|
||||
'name': 'delete other users\' posts',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'edit_book',
|
||||
'name': 'edit book info',
|
||||
'groups': ['admin', 'moderator', 'editor']
|
||||
}]
|
||||
|
||||
content_type = ContentType.objects.get_for_model(User)
|
||||
for permission in permissions:
|
||||
permission_obj = Permission.objects.create(
|
||||
codename=permission['codename'],
|
||||
name=permission['name'],
|
||||
content_type=content_type,
|
||||
)
|
||||
# add the permission to the appropriate groups
|
||||
for group_name in permission['groups']:
|
||||
Group.objects.get(name=group_name).permissions.add(permission_obj)
|
||||
|
||||
# while the groups and permissions shouldn't be changed because the code
|
||||
# depends on them, what permissions go with what groups should be editable
|
||||
|
||||
|
||||
def init_connectors():
|
||||
Connector.objects.create(
|
||||
identifier=DOMAIN,
|
||||
name='Local',
|
||||
local=True,
|
||||
connector_file='self_connector',
|
||||
base_url='https://%s' % DOMAIN,
|
||||
books_url='https://%s/book' % DOMAIN,
|
||||
covers_url='https://%s/images/covers' % DOMAIN,
|
||||
search_url='https://%s/search?q=' % DOMAIN,
|
||||
priority=1,
|
||||
)
|
||||
|
||||
Connector.objects.create(
|
||||
identifier='bookwyrm.social',
|
||||
name='BookWyrm dot Social',
|
||||
connector_file='bookwyrm_connector',
|
||||
base_url='https://bookwyrm.social',
|
||||
books_url='https://bookwyrm.social/book',
|
||||
covers_url='https://bookwyrm.social/images/covers',
|
||||
search_url='https://bookwyrm.social/search?q=',
|
||||
priority=2,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
def init_settings():
|
||||
SiteSettings.objects.create()
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Initializes the database with starter data'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
init_groups()
|
||||
init_permissions()
|
||||
init_connectors()
|
||||
init_settings()
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-02-21 17:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0005_auto_20200221_1645'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='identifier',
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
]
|
File diff suppressed because it is too large
Load diff
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-02-23 09:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0006_auto_20200221_1702'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='userrelationship',
|
||||
constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='followers_unique'),
|
||||
),
|
||||
]
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.0.3 on 2020-06-02 15:46
|
||||
# Generated by Django 3.0.7 on 2020-11-03 00:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
@ -8,14 +8,13 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0043_siteinvite'),
|
||||
('bookwyrm', '0006_auto_20200221_1702_squashed_0064_merge_20201101_1913'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
migrations.AlterField(
|
||||
model_name='siteinvite',
|
||||
name='user',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-02-24 15:04
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0007_auto_20200223_0902'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='followers',
|
||||
field=models.ManyToManyField(related_name='following', through='bookwyrm.UserRelationship', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
35
bookwyrm/migrations/0008_work_default_edition.py
Normal file
35
bookwyrm/migrations/0008_work_default_edition.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-04 18:15
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def set_default_edition(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
works = app_registry.get_model('bookwyrm', 'Work').objects.using(db_alias)
|
||||
editions = app_registry.get_model('bookwyrm', 'Edition').objects.using(db_alias)
|
||||
for work in works:
|
||||
ed = editions.filter(parent_work=work, default=True).first()
|
||||
if not ed:
|
||||
ed = editions.filter(parent_work=work).first()
|
||||
work.default_edition = ed
|
||||
work.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0007_auto_20201103_0014'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='work',
|
||||
name='default_edition',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.RunPython(set_default_edition),
|
||||
migrations.RemoveField(
|
||||
model_name='edition',
|
||||
name='default',
|
||||
),
|
||||
]
|
18
bookwyrm/migrations/0009_shelf_privacy.py
Normal file
18
bookwyrm/migrations/0009_shelf_privacy.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-10 20:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0008_work_default_edition'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shelf',
|
||||
name='privacy',
|
||||
field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-07 00:28
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0008_auto_20200224_1504'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='status',
|
||||
name='published_date',
|
||||
field=models.DateTimeField(default=datetime.datetime.now),
|
||||
),
|
||||
]
|
|
@ -1,190 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-07 06:55
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import bookwyrm.utils.fields
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0009_status_published_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Edition',
|
||||
fields=[
|
||||
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Book')),
|
||||
('isbn', models.CharField(max_length=255, null=True, unique=True)),
|
||||
('oclc_number', models.CharField(max_length=255, null=True, unique=True)),
|
||||
('pages', models.IntegerField(null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('bookwyrm.book',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Work',
|
||||
fields=[
|
||||
('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Book')),
|
||||
('lccn', models.CharField(max_length=255, null=True, unique=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('bookwyrm.book',),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='added_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='data',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='aliases',
|
||||
field=bookwyrm.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, size=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='bio',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='born',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='died',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='first_name',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='last_name',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='name',
|
||||
field=models.CharField(default='Unknown', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='wikipedia_link',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='first_published_date',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='language',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='last_sync_date',
|
||||
field=models.DateTimeField(default=datetime.datetime.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='librarything_key',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='local_edits',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='local_key',
|
||||
field=models.CharField(default=uuid.uuid4, max_length=255, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='misc_identifiers',
|
||||
field=bookwyrm.utils.fields.JSONField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='origin',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='published_date',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='series',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='series_number',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='sort_title',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='subtitle',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='sync',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='title',
|
||||
field=models.CharField(default='Unknown', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='openlibrary_key',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='openlibrary_key',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='parent_work',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Work'),
|
||||
),
|
||||
]
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-09 20:09
|
||||
# Generated by Django 3.0.7 on 2020-11-13 15:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0012_auto_20200308_1625'),
|
||||
('bookwyrm', '0009_shelf_privacy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='manually_approves_followers',
|
||||
model_name='importjob',
|
||||
name='retry',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
33
bookwyrm/migrations/0011_auto_20201113_1727.py
Normal file
33
bookwyrm/migrations/0011_auto_20201113_1727.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-13 17:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def set_origin_id(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
books = app_registry.get_model('bookwyrm', 'Book').objects.using(db_alias)
|
||||
for book in books:
|
||||
book.origin_id = book.remote_id
|
||||
# the remote_id will be set automatically
|
||||
book.remote_id = None
|
||||
book.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0010_importjob_retry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='origin_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='origin_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.RunPython(set_origin_id),
|
||||
]
|
|
@ -1,32 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-07 22:23
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0010_auto_20200307_0655'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('read', models.BooleanField(default=False)),
|
||||
('notification_type', models.CharField(max_length=255)),
|
||||
('related_book', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')),
|
||||
('related_status', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status')),
|
||||
('related_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='related_user', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
29
bookwyrm/migrations/0012_attachment.py
Normal file
29
bookwyrm/migrations/0012_attachment.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-24 19:39
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0011_auto_20201113_1727'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attachment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('remote_id', models.CharField(max_length=255, null=True)),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='status/')),
|
||||
('caption', models.TextField(blank=True, null=True)),
|
||||
('status', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-08 16:25
|
||||
|
||||
from django.db import migrations, models
|
||||
import bookwyrm.utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0011_notification'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='aliases',
|
||||
field=bookwyrm.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
),
|
||||
]
|
18
bookwyrm/migrations/0013_book_origin_id.py
Normal file
18
bookwyrm/migrations/0013_book_origin_id.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-24 21:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0012_attachment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='origin_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
17
bookwyrm/migrations/0014_auto_20201128_0118.py
Normal file
17
bookwyrm/migrations/0014_auto_20201128_0118.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-28 01:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0013_book_origin_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Attachment',
|
||||
new_name='Image',
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-10 19:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0013_user_manually_approves_followers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='status',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
]
|
|
@ -1,115 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-11 12:12
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0014_status_remote_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserBlocks',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('relationship_id', models.CharField(max_length=100)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserFollowRequest',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('relationship_id', models.CharField(max_length=100)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserFollows',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('relationship_id', models.CharField(max_length=100)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='followers',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UserRelationship',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userfollows',
|
||||
name='user_object',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_object', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userfollows',
|
||||
name='user_subject',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userfollowrequest',
|
||||
name='user_object',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_object', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userfollowrequest',
|
||||
name='user_subject',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userblocks',
|
||||
name='user_object',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_object', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userblocks',
|
||||
name='user_subject',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='blocks',
|
||||
field=models.ManyToManyField(related_name='blocked_by', through='bookwyrm.UserBlocks', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='follow_requests',
|
||||
field=models.ManyToManyField(related_name='follower_requests', through='bookwyrm.UserFollowRequest', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='following',
|
||||
field=models.ManyToManyField(related_name='followers', through='bookwyrm.UserFollows', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='userfollows',
|
||||
constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userfollows_unique'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='userfollowrequest',
|
||||
constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userfollowrequest_unique'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='userblocks',
|
||||
constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userblocks_unique'),
|
||||
),
|
||||
]
|
19
bookwyrm/migrations/0015_auto_20201128_0349.py
Normal file
19
bookwyrm/migrations/0015_auto_20201128_0349.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-28 03:49
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0014_auto_20201128_0118'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='status',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status'),
|
||||
),
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-13 13:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0015_auto_20200311_1212'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='notification_type',
|
||||
field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request')], max_length=255),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='notification',
|
||||
constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST']), name='notification_type_valid'),
|
||||
),
|
||||
]
|
63
bookwyrm/migrations/0016_auto_20201129_0304.py
Normal file
63
bookwyrm/migrations/0016_auto_20201129_0304.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-29 03:04
|
||||
|
||||
import bookwyrm.utils.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0015_auto_20201128_0349'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subject_places',
|
||||
field=bookwyrm.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subjects',
|
||||
field=bookwyrm.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='parent_work',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='editions', to='bookwyrm.Work'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, unique=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='tag',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tag',
|
||||
name='book',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tag',
|
||||
name='user',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserTag',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('remote_id', models.CharField(max_length=255, null=True)),
|
||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')),
|
||||
('tag', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Tag')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'book', 'tag')},
|
||||
},
|
||||
),
|
||||
]
|
28
bookwyrm/migrations/0016_auto_20201211_2026.py
Normal file
28
bookwyrm/migrations/0016_auto_20201211_2026.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.0.7 on 2020-12-11 20:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0015_auto_20201128_0349'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='admin_email',
|
||||
field=models.EmailField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='support_link',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='support_title',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-14 21:52
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.expressions
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0016_auto_20200313_1337'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='userblocks',
|
||||
constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userblocks_no_self'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='userfollowrequest',
|
||||
constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userfollowrequest_no_self'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='userfollows',
|
||||
constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userfollows_no_self'),
|
||||
),
|
||||
]
|
189
bookwyrm/migrations/0017_auto_20201130_1819.py
Normal file
189
bookwyrm/migrations/0017_auto_20201130_1819.py
Normal file
|
@ -0,0 +1,189 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-30 18:19
|
||||
|
||||
import bookwyrm.models.base_model
|
||||
import bookwyrm.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
def copy_rsa_keys(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
users = app_registry.get_model('bookwyrm', 'User')
|
||||
keypair = app_registry.get_model('bookwyrm', 'KeyPair')
|
||||
for user in users.objects.using(db_alias):
|
||||
if user.public_key or user.private_key:
|
||||
user.key_pair = keypair.objects.create(
|
||||
remote_id='%s/#main-key' % user.remote_id,
|
||||
private_key=user.private_key,
|
||||
public_key=user.public_key
|
||||
)
|
||||
user.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0016_auto_20201129_0304'),
|
||||
]
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KeyPair',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
|
||||
('private_key', models.TextField(blank=True, null=True)),
|
||||
('public_key', bookwyrm.models.fields.TextField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='followers',
|
||||
field=bookwyrm.models.fields.ManyToManyField(related_name='following', through='bookwyrm.UserFollows', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connector',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='favorite',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='federatedserver',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='readthrough',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='avatar',
|
||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='avatars/'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='bookwyrm_user',
|
||||
field=bookwyrm.models.fields.BooleanField(default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='inbox',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='local',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='manually_approves_followers',
|
||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='outbox',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='shared_inbox',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='summary',
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='username',
|
||||
field=bookwyrm.models.fields.UsernameField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userblocks',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollowrequest',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollows',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usertag',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='key_pair',
|
||||
field=bookwyrm.models.fields.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to='bookwyrm.KeyPair'),
|
||||
),
|
||||
migrations.RunPython(copy_rsa_keys),
|
||||
]
|
25
bookwyrm/migrations/0018_auto_20201130_1832.py
Normal file
25
bookwyrm/migrations/0018_auto_20201130_1832.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-30 18:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0017_auto_20201130_1819'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='following',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='private_key',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='public_key',
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-21 21:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0017_auto_20200314_2152'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='favorite',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
]
|
36
bookwyrm/migrations/0019_auto_20201130_1939.py
Normal file
36
bookwyrm/migrations/0019_auto_20201130_1939.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-30 19:39
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
def update_notnull(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
users = app_registry.get_model('bookwyrm', 'User')
|
||||
for user in users.objects.using(db_alias):
|
||||
if user.name and user.summary:
|
||||
continue
|
||||
if not user.summary:
|
||||
user.summary = ''
|
||||
if not user.name:
|
||||
user.name = ''
|
||||
user.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0018_auto_20201130_1832'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_notnull),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(default='', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='summary',
|
||||
field=bookwyrm.models.fields.TextField(default=''),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-21 22:43
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0018_favorite_remote_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('bookwyrm.status',),
|
||||
),
|
||||
]
|
|
@ -1,58 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-27 23:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import bookwyrm.models.book
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0019_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Connector',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('identifier', models.CharField(max_length=255, unique=True)),
|
||||
('connector_file', models.CharField(choices=[('openlibrary', 'Openlibrary'), ('bookwyrm', 'BookWyrm')], default='openlibrary', max_length=255)),
|
||||
('is_self', models.BooleanField(default=False)),
|
||||
('api_key', models.CharField(max_length=255, null=True)),
|
||||
('base_url', models.CharField(max_length=255)),
|
||||
('covers_url', models.CharField(max_length=255)),
|
||||
('search_url', models.CharField(max_length=255, null=True)),
|
||||
('key_name', models.CharField(max_length=255)),
|
||||
('politeness_delay', models.IntegerField(null=True)),
|
||||
('max_query_count', models.IntegerField(null=True)),
|
||||
('query_count', models.IntegerField(default=0)),
|
||||
('query_count_expiry', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='book',
|
||||
old_name='local_key',
|
||||
new_name='fedireads_key',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='book',
|
||||
old_name='origin',
|
||||
new_name='source_url',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='local_edits',
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='connector',
|
||||
constraint=models.CheckConstraint(check=models.Q(connector_file__in=bookwyrm.models.connector.ConnectorFiles), name='connector_file_valid'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='connector',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Connector'),
|
||||
),
|
||||
]
|
353
bookwyrm/migrations/0020_auto_20201208_0213.py
Normal file
353
bookwyrm/migrations/0020_auto_20201208_0213.py
Normal file
|
@ -0,0 +1,353 @@
|
|||
# Generated by Django 3.0.7 on 2020-12-08 02:13
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0019_auto_20201130_1939'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='aliases',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='bio',
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='born',
|
||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='died',
|
||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='openlibrary_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='wikipedia_link',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='authors',
|
||||
field=bookwyrm.models.fields.ManyToManyField(to='bookwyrm.Author'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='cover',
|
||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='covers/'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='description',
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='first_published_date',
|
||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='goodreads_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='languages',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='librarything_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='openlibrary_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='published_date',
|
||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='series',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='series_number',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='sort_title',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subject_places',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subjects',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subtitle',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='title',
|
||||
field=bookwyrm.models.fields.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boost',
|
||||
name='boosted_status',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boosters', to='bookwyrm.Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='asin',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='isbn_10',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='isbn_13',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='oclc_number',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='pages',
|
||||
field=bookwyrm.models.fields.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='parent_work',
|
||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='editions', to='bookwyrm.Work'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='physical_format',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='publishers',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='favorite',
|
||||
name='status',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='favorite',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='caption',
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='image',
|
||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='status/'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotation',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotation',
|
||||
name='quote',
|
||||
field=bookwyrm.models.fields.TextField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='rating',
|
||||
field=bookwyrm.models.fields.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='privacy',
|
||||
field=bookwyrm.models.fields.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='added_by',
|
||||
field=bookwyrm.models.fields.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='shelf',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Shelf'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='content',
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='mention_books',
|
||||
field=bookwyrm.models.fields.TagField(related_name='mention_book', to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='mention_users',
|
||||
field=bookwyrm.models.fields.TagField(related_name='mention_user', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='published_date',
|
||||
field=bookwyrm.models.fields.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='reply_parent',
|
||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='sensitive',
|
||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(max_length=100, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userblocks',
|
||||
name='user_object',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_object', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userblocks',
|
||||
name='user_subject',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollowrequest',
|
||||
name='user_object',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_object', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollowrequest',
|
||||
name='user_subject',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollows',
|
||||
name='user_object',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_object', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollows',
|
||||
name='user_subject',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usertag',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usertag',
|
||||
name='tag',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usertag',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='default_edition',
|
||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='lccn',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,44 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-28 04:28
|
||||
|
||||
from django.db import migrations, models
|
||||
import bookwyrm.utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0020_auto_20200327_2335'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='goodreads_key',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='subject_places',
|
||||
field=bookwyrm.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='subjects',
|
||||
field=bookwyrm.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='physical_format',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='publishers',
|
||||
field=bookwyrm.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connector',
|
||||
name='connector_file',
|
||||
field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('fedireads_connector', 'Fedireads Connector')], default='openlibrary', max_length=255),
|
||||
),
|
||||
]
|
14
bookwyrm/migrations/0021_merge_20201212_1737.py
Normal file
14
bookwyrm/migrations/0021_merge_20201212_1737.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 3.0.7 on 2020-12-12 17:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0020_auto_20201208_0213'),
|
||||
('bookwyrm', '0016_auto_20201211_2026'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
|
@ -1,27 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-28 20:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0021_auto_20200328_0428'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='connector',
|
||||
name='is_self',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='fedireads_key',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connector',
|
||||
name='connector_file',
|
||||
field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('fedireads_connector', 'Fedireads Connector')], default='openlibrary', max_length=255),
|
||||
),
|
||||
]
|
30
bookwyrm/migrations/0022_auto_20201212_1744.py
Normal file
30
bookwyrm/migrations/0022_auto_20201212_1744.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 3.0.7 on 2020-12-12 17:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_author_name(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
authors = app_registry.get_model('bookwyrm', 'Author')
|
||||
for author in authors.objects.using(db_alias):
|
||||
if not author.name:
|
||||
author.name = '%s %s' % (author.first_name, author.last_name)
|
||||
author.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0021_merge_20201212_1737'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_author_name),
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='first_name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='last_name',
|
||||
),
|
||||
]
|
|
@ -1,114 +0,0 @@
|
|||
# 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 = [
|
||||
('bookwyrm', '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),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-29 22:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0023_auto_20200328_2203'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='federatedserver',
|
||||
name='application_version',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,24 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-30 00:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0024_federatedserver_application_version'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='last_sync_date',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='published_date',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
|
@ -1,42 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-30 14:56
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0025_auto_20200330_0037'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Boost',
|
||||
fields=[
|
||||
('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('bookwyrm.status',),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='notification',
|
||||
name='notification_type_valid',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='notification_type',
|
||||
field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost')], max_length=255),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='notification',
|
||||
constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST']), name='notification_type_valid'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boost',
|
||||
name='boosted_status',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boosters', to='bookwyrm.Status'),
|
||||
),
|
||||
]
|
|
@ -1,82 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-30 22:32
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import bookwyrm.utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0026_auto_20200330_1456'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='language',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='parent_work',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='shelves',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='languages',
|
||||
field=bookwyrm.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='default',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='parent_work',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Work'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='shelves',
|
||||
field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Shelf'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='book',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='related_book',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='book',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='books',
|
||||
field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='book',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='mention_books',
|
||||
field=models.ManyToManyField(related_name='mention_book', to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='book',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-01 18:24
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0027_auto_20200330_2232'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='name',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='rating',
|
||||
field=models.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-03 18:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0028_auto_20200401_1824'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-07 00:51
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0029_auto_20200403_1835'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Quotation',
|
||||
fields=[
|
||||
('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')),
|
||||
('quote', models.TextField()),
|
||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('bookwyrm.status',),
|
||||
),
|
||||
]
|
|
@ -1,31 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-15 12:24
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0030_quotation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ReadThrough',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('pages_read', models.IntegerField(blank=True, null=True)),
|
||||
('start_date', models.DateTimeField(blank=True, null=True)),
|
||||
('finish_date', models.DateTimeField(blank=True, null=True)),
|
||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,60 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-21 13:47
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import bookwyrm.utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0031_readthrough'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ImportItem',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('data', bookwyrm.utils.fields.JSONField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ImportJob',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('task_id', models.CharField(max_length=100, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='notification',
|
||||
name='notification_type_valid',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='notification_type',
|
||||
field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT_RESULT', 'Import Result')], max_length=255),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='notification',
|
||||
constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT_RESULT']), name='notification_type_valid'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='importjob',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='importitem',
|
||||
name='book',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='bookwyrm.Book'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='importitem',
|
||||
name='job',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='bookwyrm.ImportJob'),
|
||||
),
|
||||
]
|
|
@ -1,43 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-22 12:49
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0032_auto_20200421_1347'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='notification',
|
||||
name='notification_type_valid',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='importitem',
|
||||
name='fail_reason',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='importitem',
|
||||
name='index',
|
||||
field=models.IntegerField(default=1),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='related_import',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.ImportJob'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='notification_type',
|
||||
field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import')], max_length=255),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='notification',
|
||||
constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT']), name='notification_type_valid'),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-22 13:12
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0033_auto_20200422_1249'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='importjob',
|
||||
name='import_status',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'),
|
||||
),
|
||||
]
|
|
@ -1,33 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-04-29 17:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0034_importjob_import_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='edition',
|
||||
old_name='isbn',
|
||||
new_name='isbn_13',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='author_text',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='asin',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='isbn_10',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,39 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-03 20:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0035_auto_20200429_1708'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='connector',
|
||||
name='books_url',
|
||||
field=models.CharField(default='https://openlibrary.org', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connector',
|
||||
name='local',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connector',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connector',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=2),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connector',
|
||||
name='connector_file',
|
||||
field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('fedireads_connector', 'Fedireads Connector')], max_length=255),
|
||||
),
|
||||
]
|
|
@ -1,41 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-04 01:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0036_auto_20200503_2007'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='fedireads_key',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='fedireads_key',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='source_url',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='last_sync_date',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='sync',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-09 19:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0037_auto_20200504_0154'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,21 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-10 23:42
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0038_author_remote_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='misc_identifiers',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='connector',
|
||||
name='key_name',
|
||||
),
|
||||
]
|
|
@ -1,77 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-13 01:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0039_auto_20200510_2342'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='actor',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='connector',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='federatedserver',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='readthrough',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shelf',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shelfbook',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userblocks',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userfollowrequest',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userfollows',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='favorite',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-13 02:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0040_auto_20200513_0153'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='remote_id',
|
||||
field=models.CharField(max_length=255, null=True, unique=True),
|
||||
),
|
||||
]
|
|
@ -1,21 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-24 03:46
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0041_user_remote_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='status',
|
||||
name='activity_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='status',
|
||||
name='status_type',
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-06-01 18:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0041_user_remote_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteSettings',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='wyrms.cthulahoops.org', max_length=100)),
|
||||
('instance_description', models.TextField(default='This instance has no description.')),
|
||||
('code_of_conduct', models.TextField(default='Add a code of conduct here.')),
|
||||
('allow_registration', models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,24 +0,0 @@
|
|||
# Generated by Django 3.0.3 on 2020-06-01 21:31
|
||||
|
||||
from django.db import migrations, models
|
||||
import bookwyrm.models.site
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0042_sitesettings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteInvite',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(default=bookwyrm.models.site.new_access_code, max_length=32)),
|
||||
('expiry', models.DateTimeField(blank=True, null=True)),
|
||||
('use_limit', models.IntegerField(blank=True, null=True)),
|
||||
('times_used', models.IntegerField(default=0)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,14 +0,0 @@
|
|||
# Generated by Django 3.0.7 on 2020-08-10 20:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0044_siteinvite_user'),
|
||||
('bookwyrm', '0042_auto_20200524_0346'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
# Generated by Django 3.0.7 on 2020-09-21 15:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0045_merge_20200810_2010'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='user',
|
||||
old_name='fedireads_user',
|
||||
new_name='bookwyrm_user',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connector',
|
||||
name='connector_file',
|
||||
field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('bookwyrm_connector', 'BookWyrm Connector')], max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sitesettings',
|
||||
name='name',
|
||||
field=models.CharField(default='1d8390fd.ngrok.io', max_length=100),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.7 on 2020-09-28 23:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0046_auto_20200921_1509'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='connector',
|
||||
name='connector_file',
|
||||
field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('bookwyrm_connector', 'Bookwyrm Connector')], max_length=255),
|
||||
),
|
||||
]
|
|
@ -1,24 +0,0 @@
|
|||
# Generated by Django 3.0.7 on 2020-09-29 00:22
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0047_auto_20200928_2312'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GeneratedStatus',
|
||||
fields=[
|
||||
('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('bookwyrm.status',),
|
||||
),
|
||||
]
|
|
@ -1,25 +0,0 @@
|
|||
# Generated by Django 3.0.7 on 2020-10-02 19:43
|
||||
|
||||
import bookwyrm.models.site
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0048_generatednote'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PasswordReset',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(default=bookwyrm.models.site.new_access_code, max_length=32)),
|
||||
('expiry', models.DateTimeField(default=bookwyrm.models.site.get_passowrd_reset_expiry)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.7 on 2020-10-02 21:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0049_passwordreset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, unique=True),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.7 on 2020-10-05 21:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0050_auto_20201002_2156'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sitesettings',
|
||||
name='name',
|
||||
field=models.CharField(default='BookWyrm', max_length=100),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 3.0.7 on 2020-10-05 21:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0051_auto_20201005_2142'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name='email address'),
|
||||
),
|
||||
]
|
|
@ -2,19 +2,31 @@
|
|||
import inspect
|
||||
import sys
|
||||
|
||||
from .book import Book, Work, Edition, Author
|
||||
from .book import Book, Work, Edition
|
||||
from .author import Author
|
||||
from .connector import Connector
|
||||
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
||||
|
||||
from .shelf import Shelf, ShelfBook
|
||||
from .status import Status, GeneratedStatus, Review, Comment, Quotation
|
||||
|
||||
from .status import Status, GeneratedNote, Review, Comment, Quotation
|
||||
from .status import Favorite, Boost, Notification, ReadThrough
|
||||
from .tag import Tag
|
||||
from .user import User
|
||||
from .attachment import Image
|
||||
|
||||
from .tag import Tag, UserTag
|
||||
|
||||
from .user import User, KeyPair
|
||||
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
||||
from .federated_server import FederatedServer
|
||||
|
||||
from .import_job import ImportJob, ImportItem
|
||||
|
||||
from .site import SiteSettings, SiteInvite, PasswordReset
|
||||
|
||||
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
||||
activity_models = {c[0]: c[1].activity_serializer for c in cls_members \
|
||||
if hasattr(c[1], 'activity_serializer')}
|
||||
activity_models = {c[1].activity_serializer.__name__: c[1] \
|
||||
for c in cls_members if hasattr(c[1], 'activity_serializer')}
|
||||
|
||||
def to_activity(activity_json):
|
||||
''' link up models and activities '''
|
||||
activity_type = activity_json.get('type')
|
||||
return activity_models[activity_type].to_activity(activity_json)
|
||||
|
|
30
bookwyrm/models/attachment.py
Normal file
30
bookwyrm/models/attachment.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
''' media that is posted in the app '''
|
||||
from django.db import models
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import ActivitypubMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
class Attachment(ActivitypubMixin, BookWyrmModel):
|
||||
''' an image (or, in the future, video etc) associated with a status '''
|
||||
status = models.ForeignKey(
|
||||
'Status',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='attachments',
|
||||
null=True
|
||||
)
|
||||
reverse_unfurl = True
|
||||
class Meta:
|
||||
''' one day we'll have other types of attachments besides images '''
|
||||
abstract = True
|
||||
|
||||
|
||||
class Image(Attachment):
|
||||
''' an image attachment '''
|
||||
image = fields.ImageField(
|
||||
upload_to='status/', null=True, blank=True, activitypub_field='url')
|
||||
caption = fields.TextField(null=True, blank=True, activitypub_field='name')
|
||||
|
||||
activity_serializer = activitypub.Image
|
43
bookwyrm/models/author.py
Normal file
43
bookwyrm/models/author.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
''' database schema for info about authors '''
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
class Author(ActivitypubMixin, BookWyrmModel):
|
||||
''' basic biographic info '''
|
||||
origin_id = models.CharField(max_length=255, null=True)
|
||||
openlibrary_key = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
sync = models.BooleanField(default=True)
|
||||
last_sync_date = models.DateTimeField(default=timezone.now)
|
||||
wikipedia_link = fields.CharField(max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
# idk probably other keys would be useful here?
|
||||
born = fields.DateTimeField(blank=True, null=True)
|
||||
died = fields.DateTimeField(blank=True, null=True)
|
||||
name = fields.CharField(max_length=255)
|
||||
aliases = fields.ArrayField(
|
||||
models.CharField(max_length=255), blank=True, default=list
|
||||
)
|
||||
bio = fields.TextField(null=True, blank=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' can't be abstract for query reasons, but you shouldn't USE it '''
|
||||
if self.id and not self.remote_id:
|
||||
self.remote_id = self.get_remote_id()
|
||||
|
||||
if not self.id:
|
||||
self.origin_id = self.remote_id
|
||||
self.remote_id = None
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_remote_id(self):
|
||||
''' editions and works both use "book" instead of model_name '''
|
||||
return 'https://%s/author/%s' % (DOMAIN, self.id)
|
||||
|
||||
activity_serializer = activitypub.Author
|
|
@ -1,24 +1,34 @@
|
|||
''' base model with default fields '''
|
||||
from base64 import b64encode
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
from functools import reduce
|
||||
import operator
|
||||
from uuid import uuid4
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import pkcs1_15
|
||||
from Crypto.Hash import SHA256
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.dispatch import receiver
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.settings import DOMAIN, PAGE_LENGTH
|
||||
from .fields import RemoteIdField
|
||||
|
||||
|
||||
PrivacyLevels = models.TextChoices('Privacy', [
|
||||
'public',
|
||||
'unlisted',
|
||||
'followers',
|
||||
'direct'
|
||||
])
|
||||
|
||||
class BookWyrmModel(models.Model):
|
||||
''' shared fields '''
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
updated_date = models.DateTimeField(auto_now=True)
|
||||
remote_id = models.CharField(max_length=255, null=True)
|
||||
remote_id = RemoteIdField(null=True, activitypub_field='id')
|
||||
|
||||
def get_remote_id(self):
|
||||
''' generate a url that resolves to the local object '''
|
||||
|
@ -43,40 +53,99 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
|||
instance.save()
|
||||
|
||||
|
||||
def unfurl_related_field(related_field):
|
||||
''' load reverse lookups (like public key owner or Status attachment '''
|
||||
if hasattr(related_field, 'all'):
|
||||
return [unfurl_related_field(i) for i in related_field.all()]
|
||||
if related_field.reverse_unfurl:
|
||||
return related_field.field_to_activity()
|
||||
return related_field.remote_id
|
||||
|
||||
|
||||
class ActivitypubMixin:
|
||||
''' add this mixin for models that are AP serializable '''
|
||||
activity_serializer = lambda: {}
|
||||
reverse_unfurl = False
|
||||
|
||||
def to_activity(self, pure=False):
|
||||
''' convert from a model to an activity '''
|
||||
if pure:
|
||||
mappings = self.pure_activity_mappings
|
||||
else:
|
||||
mappings = self.activity_mappings
|
||||
@classmethod
|
||||
def find_existing_by_remote_id(cls, remote_id):
|
||||
''' look up a remote id in the db '''
|
||||
return cls.find_existing({'id': remote_id})
|
||||
|
||||
fields = {}
|
||||
for mapping in mappings:
|
||||
if not hasattr(self, mapping.model_key) or not mapping.activity_key:
|
||||
@classmethod
|
||||
def find_existing(cls, data):
|
||||
''' compare data to fields that can be used for deduplation.
|
||||
This always includes remote_id, but can also be unique identifiers
|
||||
like an isbn for an edition '''
|
||||
filters = []
|
||||
for field in cls._meta.get_fields():
|
||||
if not hasattr(field, 'deduplication_field') or \
|
||||
not field.deduplication_field:
|
||||
continue
|
||||
value = getattr(self, mapping.model_key)
|
||||
if hasattr(value, 'remote_id'):
|
||||
value = value.remote_id
|
||||
fields[mapping.activity_key] = mapping.activity_formatter(value)
|
||||
|
||||
if pure:
|
||||
return self.pure_activity_serializer(
|
||||
**fields
|
||||
).serialize()
|
||||
return self.activity_serializer(
|
||||
**fields
|
||||
).serialize()
|
||||
value = data.get(field.activitypub_field)
|
||||
if not value:
|
||||
continue
|
||||
filters.append({field.name: value})
|
||||
|
||||
if hasattr(cls, 'origin_id') and 'id' in data:
|
||||
# kinda janky, but this handles special case for books
|
||||
filters.append({'origin_id': data['id']})
|
||||
|
||||
if not filters:
|
||||
# if there are no deduplication fields, it will match the first
|
||||
# item no matter what. this shouldn't happen but just in case.
|
||||
return None
|
||||
|
||||
objects = cls.objects
|
||||
if hasattr(objects, 'select_subclasses'):
|
||||
objects = objects.select_subclasses()
|
||||
|
||||
# an OR operation on all the match fields
|
||||
match = objects.filter(
|
||||
reduce(
|
||||
operator.or_, (Q(**f) for f in filters)
|
||||
)
|
||||
)
|
||||
# there OUGHT to be only one match
|
||||
return match.first()
|
||||
|
||||
|
||||
def to_create_activity(self, user, pure=False):
|
||||
def to_activity(self):
|
||||
''' convert from a model to an activity '''
|
||||
activity = {}
|
||||
for field in self._meta.get_fields():
|
||||
if not hasattr(field, 'field_to_activity'):
|
||||
continue
|
||||
value = field.field_to_activity(getattr(self, field.name))
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
key = field.get_activitypub_field()
|
||||
if key in activity and isinstance(activity[key], list):
|
||||
# handles tags on status, which accumulate across fields
|
||||
activity[key] += value
|
||||
else:
|
||||
activity[key] = value
|
||||
|
||||
if hasattr(self, 'serialize_reverse_fields'):
|
||||
# for example, editions of a work
|
||||
for model_field_name, activity_field_name in \
|
||||
self.serialize_reverse_fields:
|
||||
related_field = getattr(self, model_field_name)
|
||||
activity[activity_field_name] = \
|
||||
unfurl_related_field(related_field)
|
||||
|
||||
if not activity.get('id'):
|
||||
activity['id'] = self.get_remote_id()
|
||||
return self.activity_serializer(**activity).serialize()
|
||||
|
||||
|
||||
def to_create_activity(self, user):
|
||||
''' returns the object wrapped in a Create activity '''
|
||||
activity_object = self.to_activity(pure=pure)
|
||||
activity_object = self.to_activity()
|
||||
|
||||
signer = pkcs1_15.new(RSA.import_key(user.private_key))
|
||||
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
|
||||
content = activity_object['content']
|
||||
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
|
||||
create_id = self.remote_id + '/activity'
|
||||
|
@ -90,16 +159,27 @@ class ActivitypubMixin:
|
|||
return activitypub.Create(
|
||||
id=create_id,
|
||||
actor=user.remote_id,
|
||||
to=['%s/followers' % user.remote_id],
|
||||
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
||||
to=activity_object['to'],
|
||||
cc=activity_object['cc'],
|
||||
object=activity_object,
|
||||
signature=signature,
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_delete_activity(self, user):
|
||||
''' notice of deletion '''
|
||||
return activitypub.Delete(
|
||||
id=self.remote_id + '/activity',
|
||||
actor=user.remote_id,
|
||||
to=['%s/followers' % user.remote_id],
|
||||
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object=self.to_activity(),
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_update_activity(self, user):
|
||||
''' wrapper for Updates to an activity '''
|
||||
activity_id = '%s#update/%s' % (user.remote_id, uuid4())
|
||||
activity_id = '%s#update/%s' % (self.remote_id, uuid4())
|
||||
return activitypub.Update(
|
||||
id=activity_id,
|
||||
actor=user.remote_id,
|
||||
|
@ -111,10 +191,10 @@ class ActivitypubMixin:
|
|||
def to_undo_activity(self, user):
|
||||
''' undo an action '''
|
||||
return activitypub.Undo(
|
||||
id='%s#undo' % user.remote_id,
|
||||
id='%s#undo' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.to_activity()
|
||||
)
|
||||
).serialize()
|
||||
|
||||
|
||||
class OrderedCollectionPageMixin(ActivitypubMixin):
|
||||
|
@ -125,77 +205,53 @@ class OrderedCollectionPageMixin(ActivitypubMixin):
|
|||
''' this can be overriden if there's a special remote id, ie outbox '''
|
||||
return self.remote_id
|
||||
|
||||
def page(self, min_id=None, max_id=None):
|
||||
''' helper function to create the pagination url '''
|
||||
params = {'page': 'true'}
|
||||
if min_id:
|
||||
params['min_id'] = min_id
|
||||
if max_id:
|
||||
params['max_id'] = max_id
|
||||
return '?%s' % urlencode(params)
|
||||
|
||||
def next_page(self, items):
|
||||
''' use the max id of the last item '''
|
||||
if not items.count():
|
||||
return ''
|
||||
return self.page(max_id=items[items.count() - 1].id)
|
||||
|
||||
def prev_page(self, items):
|
||||
''' use the min id of the first item '''
|
||||
if not items.count():
|
||||
return ''
|
||||
return self.page(min_id=items[0].id)
|
||||
|
||||
def to_ordered_collection_page(self, queryset, remote_id, \
|
||||
id_only=False, min_id=None, max_id=None):
|
||||
''' serialize and pagiante a queryset '''
|
||||
# TODO: weird place to define this
|
||||
limit = 20
|
||||
# filters for use in the django queryset min/max
|
||||
filters = {}
|
||||
if min_id is not None:
|
||||
filters['id__gt'] = min_id
|
||||
if max_id is not None:
|
||||
filters['id__lte'] = max_id
|
||||
page_id = self.page(min_id=min_id, max_id=max_id)
|
||||
|
||||
items = queryset.filter(
|
||||
**filters
|
||||
).all()[:limit]
|
||||
|
||||
if id_only:
|
||||
page = [s.remote_id for s in items]
|
||||
else:
|
||||
page = [s.to_activity() for s in items]
|
||||
return activitypub.OrderedCollectionPage(
|
||||
id='%s%s' % (remote_id, page_id),
|
||||
partOf=remote_id,
|
||||
orderedItems=page,
|
||||
next='%s%s' % (remote_id, self.next_page(items)),
|
||||
prev='%s%s' % (remote_id, self.prev_page(items))
|
||||
).serialize()
|
||||
|
||||
def to_ordered_collection(self, queryset, \
|
||||
remote_id=None, page=False, **kwargs):
|
||||
''' an ordered collection of whatevers '''
|
||||
remote_id = remote_id or self.remote_id
|
||||
if page:
|
||||
return self.to_ordered_collection_page(
|
||||
return to_ordered_collection_page(
|
||||
queryset, remote_id, **kwargs)
|
||||
name = ''
|
||||
if hasattr(self, 'name'):
|
||||
name = self.name
|
||||
name = self.name if hasattr(self, 'name') else None
|
||||
owner = self.user.remote_id if hasattr(self, 'user') else ''
|
||||
|
||||
size = queryset.count()
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
return activitypub.OrderedCollection(
|
||||
id=remote_id,
|
||||
totalItems=size,
|
||||
totalItems=paginated.count,
|
||||
name=name,
|
||||
first='%s%s' % (remote_id, self.page()),
|
||||
last='%s%s' % (remote_id, self.page(min_id=0))
|
||||
owner=owner,
|
||||
first='%s?page=1' % remote_id,
|
||||
last='%s?page=%d' % (remote_id, paginated.num_pages)
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_ordered_collection_page(queryset, remote_id, id_only=False, page=1):
|
||||
''' serialize and pagiante a queryset '''
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
|
||||
activity_page = paginated.page(page)
|
||||
if id_only:
|
||||
items = [s.remote_id for s in activity_page.object_list]
|
||||
else:
|
||||
items = [s.to_activity() for s in activity_page.object_list]
|
||||
|
||||
prev_page = next_page = None
|
||||
if activity_page.has_next():
|
||||
next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
|
||||
if activity_page.has_previous():
|
||||
prev_page = '%s?page=%d' % \
|
||||
(remote_id, activity_page.previous_page_number())
|
||||
return activitypub.OrderedCollectionPage(
|
||||
id='%s?page=%s' % (remote_id, page),
|
||||
partOf=remote_id,
|
||||
orderedItems=items,
|
||||
next=next_page,
|
||||
prev=prev_page
|
||||
).serialize()
|
||||
|
||||
|
||||
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||
''' extends activitypub models to work as ordered collections '''
|
||||
@property
|
||||
|
@ -208,12 +264,3 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
|||
def to_activity(self, **kwargs):
|
||||
''' an ordered collection of the specified model queryset '''
|
||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActivityMapping:
|
||||
''' translate between an activitypub json field and a model field '''
|
||||
activity_key: str
|
||||
model_key: str
|
||||
activity_formatter: Callable = lambda x: x
|
||||
model_formatter: Callable = lambda x: x
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
''' database schema for books and shelves '''
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.http import http_date
|
||||
from model_utils.managers import InheritanceManager
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.utils.fields import ArrayField
|
||||
|
||||
from .base_model import ActivityMapping, ActivitypubMixin, BookWyrmModel
|
||||
|
||||
from .base_model import BookWyrmModel
|
||||
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
|
||||
from . import fields
|
||||
|
||||
class Book(ActivitypubMixin, BookWyrmModel):
|
||||
''' a generic book, which can mean either an edition or a work '''
|
||||
origin_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
# these identifiers apply to both works and editions
|
||||
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)
|
||||
openlibrary_key = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
librarything_key = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
goodreads_key = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
|
||||
# info about where the data comes from and where/if to sync
|
||||
sync = models.BooleanField(default=True)
|
||||
|
@ -28,97 +33,48 @@ class Book(ActivitypubMixin, BookWyrmModel):
|
|||
# TODO: edit history
|
||||
|
||||
# book/work metadata
|
||||
title = models.CharField(max_length=255)
|
||||
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)
|
||||
languages = ArrayField(
|
||||
title = fields.CharField(max_length=255)
|
||||
sort_title = fields.CharField(max_length=255, blank=True, null=True)
|
||||
subtitle = fields.CharField(max_length=255, blank=True, null=True)
|
||||
description = fields.TextField(blank=True, null=True)
|
||||
languages = fields.ArrayField(
|
||||
models.CharField(max_length=255), blank=True, default=list
|
||||
)
|
||||
series = models.CharField(max_length=255, blank=True, null=True)
|
||||
series_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
subjects = ArrayField(
|
||||
models.CharField(max_length=255), blank=True, default=list
|
||||
series = fields.CharField(max_length=255, blank=True, null=True)
|
||||
series_number = fields.CharField(max_length=255, blank=True, null=True)
|
||||
subjects = fields.ArrayField(
|
||||
models.CharField(max_length=255), blank=True, null=True, default=list
|
||||
)
|
||||
subject_places = ArrayField(
|
||||
models.CharField(max_length=255), blank=True, default=list
|
||||
subject_places = fields.ArrayField(
|
||||
models.CharField(max_length=255), blank=True, null=True, default=list
|
||||
)
|
||||
# TODO: include an annotation about the type of authorship (ie, translator)
|
||||
authors = models.ManyToManyField('Author')
|
||||
authors = fields.ManyToManyField('Author')
|
||||
# preformatted authorship string for search and easier display
|
||||
author_text = models.CharField(max_length=255, blank=True, null=True)
|
||||
cover = models.ImageField(upload_to='covers/', blank=True, null=True)
|
||||
first_published_date = models.DateTimeField(blank=True, null=True)
|
||||
published_date = models.DateTimeField(blank=True, null=True)
|
||||
cover = fields.ImageField(upload_to='covers/', blank=True, null=True)
|
||||
first_published_date = fields.DateTimeField(blank=True, null=True)
|
||||
published_date = fields.DateTimeField(blank=True, null=True)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def ap_authors(self):
|
||||
''' the activitypub serialization should be a list of author ids '''
|
||||
return [a.remote_id for a in self.authors.all()]
|
||||
|
||||
activity_mappings = [
|
||||
ActivityMapping('id', 'remote_id'),
|
||||
|
||||
ActivityMapping('authors', 'ap_authors'),
|
||||
ActivityMapping(
|
||||
'first_published_date',
|
||||
'first_published_date',
|
||||
activity_formatter=lambda d: http_date(d.timestamp()) if d else None
|
||||
),
|
||||
ActivityMapping(
|
||||
'published_date',
|
||||
'published_date',
|
||||
activity_formatter=lambda d: http_date(d.timestamp()) if d else None
|
||||
),
|
||||
|
||||
ActivityMapping('title', 'title'),
|
||||
ActivityMapping('sort_title', 'sort_title'),
|
||||
ActivityMapping('subtitle', 'subtitle'),
|
||||
ActivityMapping('description', 'description'),
|
||||
ActivityMapping('languages', 'languages'),
|
||||
ActivityMapping('series', 'series'),
|
||||
ActivityMapping('series_number', 'series_number'),
|
||||
ActivityMapping('subjects', 'subjects'),
|
||||
ActivityMapping('subject_places', 'subject_places'),
|
||||
|
||||
ActivityMapping('openlibrary_key', 'openlibrary_key'),
|
||||
ActivityMapping('librarything_key', 'librarything_key'),
|
||||
ActivityMapping('goodreads_key', 'goodreads_key'),
|
||||
|
||||
ActivityMapping('work', 'parent_work'),
|
||||
ActivityMapping('isbn_10', 'isbn_10'),
|
||||
ActivityMapping('isbn_13', 'isbn_13'),
|
||||
ActivityMapping('oclc_number', 'oclc_number'),
|
||||
ActivityMapping('asin', 'asin'),
|
||||
ActivityMapping('pages', 'pages'),
|
||||
ActivityMapping('physical_format', 'physical_format'),
|
||||
ActivityMapping('publishers', 'publishers'),
|
||||
|
||||
ActivityMapping('lccn', 'lccn'),
|
||||
ActivityMapping('editions', 'editions_path'),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' can't be abstract for query reasons, but you shouldn't USE it '''
|
||||
if not isinstance(self, Edition) and not isinstance(self, Work):
|
||||
raise ValueError('Books should be added as Editions or Works')
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
if self.id and not self.remote_id:
|
||||
self.remote_id = self.get_remote_id()
|
||||
|
||||
if not self.id:
|
||||
self.origin_id = self.remote_id
|
||||
self.remote_id = None
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_remote_id(self):
|
||||
''' editions and works both use "book" instead of model_name '''
|
||||
return 'https://%s/book/%d' % (DOMAIN, self.id)
|
||||
|
||||
|
||||
@property
|
||||
def local_id(self):
|
||||
''' when a book is ingested from an outside source, it becomes local to
|
||||
an instance, so it needs a local url for federation. but it still needs
|
||||
the remote_id for easier deduplication and, if appropriate, to sync with
|
||||
the remote canonical copy '''
|
||||
return 'https://%s/book/%d' % (DOMAIN, self.id)
|
||||
|
||||
def __repr__(self):
|
||||
return "<{} key={!r} title={!r}>".format(
|
||||
self.__class__,
|
||||
|
@ -127,41 +83,41 @@ class Book(ActivitypubMixin, BookWyrmModel):
|
|||
)
|
||||
|
||||
|
||||
class Work(Book):
|
||||
class Work(OrderedCollectionPageMixin, 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, blank=True, null=True)
|
||||
lccn = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
# this has to be nullable but should never be null
|
||||
default_edition = fields.ForeignKey(
|
||||
'Edition',
|
||||
on_delete=models.PROTECT,
|
||||
null=True
|
||||
)
|
||||
|
||||
@property
|
||||
def editions_path(self):
|
||||
''' it'd be nice to serialize the edition instead but, recursion '''
|
||||
return self.remote_id + '/editions'
|
||||
|
||||
|
||||
@property
|
||||
def default_edition(self):
|
||||
''' best-guess attempt at picking the default edition for this work '''
|
||||
ed = Edition.objects.filter(parent_work=self, default=True).first()
|
||||
if not ed:
|
||||
ed = Edition.objects.filter(parent_work=self).first()
|
||||
return ed
|
||||
def get_default_edition(self):
|
||||
''' in case the default edition is not set '''
|
||||
return self.default_edition or self.editions.first()
|
||||
|
||||
activity_serializer = activitypub.Work
|
||||
serialize_reverse_fields = [('editions', 'editions')]
|
||||
deserialize_reverse_fields = [('editions', 'editions')]
|
||||
|
||||
|
||||
class Edition(Book):
|
||||
''' an edition of a book '''
|
||||
# default -> this is what gets displayed for a work
|
||||
default = models.BooleanField(default=False)
|
||||
|
||||
# these identifiers only apply to editions, not works
|
||||
isbn_10 = models.CharField(max_length=255, blank=True, null=True)
|
||||
isbn_13 = models.CharField(max_length=255, blank=True, null=True)
|
||||
oclc_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
asin = 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(
|
||||
isbn_10 = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
isbn_13 = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
oclc_number = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
asin = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
||||
pages = fields.IntegerField(blank=True, null=True)
|
||||
physical_format = fields.CharField(max_length=255, blank=True, null=True)
|
||||
publishers = fields.ArrayField(
|
||||
models.CharField(max_length=255), blank=True, default=list
|
||||
)
|
||||
shelves = models.ManyToManyField(
|
||||
|
@ -170,55 +126,61 @@ class Edition(Book):
|
|||
through='ShelfBook',
|
||||
through_fields=('book', 'shelf')
|
||||
)
|
||||
parent_work = models.ForeignKey('Work', on_delete=models.PROTECT, null=True)
|
||||
parent_work = fields.ForeignKey(
|
||||
'Work', on_delete=models.PROTECT, null=True,
|
||||
related_name='editions', activitypub_field='work')
|
||||
|
||||
activity_serializer = activitypub.Edition
|
||||
name_field = 'title'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' calculate isbn 10/13 '''
|
||||
if self.isbn_13 and self.isbn_13[:3] == '978' and not self.isbn_10:
|
||||
self.isbn_10 = isbn_13_to_10(self.isbn_13)
|
||||
if self.isbn_10 and not self.isbn_13:
|
||||
self.isbn_13 = isbn_10_to_13(self.isbn_10)
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Author(ActivitypubMixin, BookWyrmModel):
|
||||
''' copy of an author from OL '''
|
||||
openlibrary_key = models.CharField(max_length=255, blank=True, null=True)
|
||||
sync = models.BooleanField(default=True)
|
||||
last_sync_date = models.DateTimeField(default=timezone.now)
|
||||
wikipedia_link = models.CharField(max_length=255, blank=True, null=True)
|
||||
# idk probably other keys would be useful here?
|
||||
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, 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
|
||||
)
|
||||
bio = models.TextField(null=True, blank=True)
|
||||
def isbn_10_to_13(isbn_10):
|
||||
''' convert an isbn 10 into an isbn 13 '''
|
||||
isbn_10 = re.sub(r'[^0-9X]', '', isbn_10)
|
||||
# drop the last character of the isbn 10 number (the original checkdigit)
|
||||
converted = isbn_10[:9]
|
||||
# add "978" to the front
|
||||
converted = '978' + converted
|
||||
# add a check digit to the end
|
||||
# multiply the odd digits by 1 and the even digits by 3 and sum them
|
||||
try:
|
||||
checksum = sum(int(i) for i in converted[::2]) + \
|
||||
sum(int(i) * 3 for i in converted[1::2])
|
||||
except ValueError:
|
||||
return None
|
||||
# add the checksum mod 10 to the end
|
||||
checkdigit = checksum % 10
|
||||
if checkdigit != 0:
|
||||
checkdigit = 10 - checkdigit
|
||||
return converted + str(checkdigit)
|
||||
|
||||
@property
|
||||
def local_id(self):
|
||||
''' when a book is ingested from an outside source, it becomes local to
|
||||
an instance, so it needs a local url for federation. but it still needs
|
||||
the remote_id for easier deduplication and, if appropriate, to sync with
|
||||
the remote canonical copy (ditto here for author)'''
|
||||
return 'https://%s/book/%d' % (DOMAIN, self.id)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
''' Helper to return a displayable name'''
|
||||
if self.name:
|
||||
return self.name
|
||||
# don't want to return a spurious space if all of these are None
|
||||
if self.first_name and self.last_name:
|
||||
return self.first_name + ' ' + self.last_name
|
||||
return self.last_name or self.first_name
|
||||
def isbn_13_to_10(isbn_13):
|
||||
''' convert isbn 13 to 10, if possible '''
|
||||
if isbn_13[:3] != '978':
|
||||
return None
|
||||
|
||||
activity_mappings = [
|
||||
ActivityMapping('id', 'remote_id'),
|
||||
ActivityMapping('url', 'remote_id'),
|
||||
ActivityMapping('name', 'display_name'),
|
||||
ActivityMapping('born', 'born'),
|
||||
ActivityMapping('died', 'died'),
|
||||
ActivityMapping('aliases', 'aliases'),
|
||||
ActivityMapping('bio', 'bio'),
|
||||
ActivityMapping('openlibrary_key', 'openlibrary_key'),
|
||||
ActivityMapping('wikipedia_link', 'wikipedia_link'),
|
||||
]
|
||||
activity_serializer = activitypub.Author
|
||||
isbn_13 = re.sub(r'[^0-9X]', '', isbn_13)
|
||||
|
||||
# remove '978' and old checkdigit
|
||||
converted = isbn_13[3:-1]
|
||||
# calculate checkdigit
|
||||
# multiple each digit by 10,9,8.. successively and sum them
|
||||
try:
|
||||
checksum = sum(int(d) * (10 - idx) for (idx, d) in enumerate(converted))
|
||||
except ValueError:
|
||||
return None
|
||||
checkdigit = checksum % 11
|
||||
checkdigit = 11 - checkdigit
|
||||
if checkdigit == 10:
|
||||
checkdigit = 'X'
|
||||
return converted + str(checkdigit)
|
||||
|
|
|
@ -10,25 +10,25 @@ class Connector(BookWyrmModel):
|
|||
''' book data source connectors '''
|
||||
identifier = models.CharField(max_length=255, unique=True)
|
||||
priority = models.IntegerField(default=2)
|
||||
name = models.CharField(max_length=255, null=True)
|
||||
name = models.CharField(max_length=255, null=True, blank=True)
|
||||
local = models.BooleanField(default=False)
|
||||
connector_file = models.CharField(
|
||||
max_length=255,
|
||||
choices=ConnectorFiles.choices
|
||||
)
|
||||
api_key = models.CharField(max_length=255, null=True)
|
||||
api_key = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
base_url = models.CharField(max_length=255)
|
||||
books_url = models.CharField(max_length=255)
|
||||
covers_url = models.CharField(max_length=255)
|
||||
search_url = models.CharField(max_length=255, null=True)
|
||||
search_url = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
politeness_delay = models.IntegerField(null=True) #seconds
|
||||
max_query_count = models.IntegerField(null=True)
|
||||
politeness_delay = models.IntegerField(null=True, blank=True) #seconds
|
||||
max_query_count = models.IntegerField(null=True, blank=True)
|
||||
# how many queries executed in a unit of time, like a day
|
||||
query_count = models.IntegerField(default=0)
|
||||
# when to reset the query count back to 0 (ie, after 1 day)
|
||||
query_count_expiry = models.DateTimeField(auto_now_add=True)
|
||||
query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
''' check that there's code to actually use this connector '''
|
||||
|
@ -38,3 +38,9 @@ class Connector(BookWyrmModel):
|
|||
name='connector_file_valid'
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "{} ({})".format(
|
||||
self.identifier,
|
||||
self.id,
|
||||
)
|
||||
|
|
273
bookwyrm/models/fields.py
Normal file
273
bookwyrm/models/fields.py
Normal file
|
@ -0,0 +1,273 @@
|
|||
''' activitypub-aware django model fields '''
|
||||
import re
|
||||
from uuid import uuid4
|
||||
|
||||
import dateutil.parser
|
||||
from dateutil.parser import ParserError
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.connectors import get_image
|
||||
|
||||
|
||||
def validate_remote_id(value):
|
||||
''' make sure the remote_id looks like a url '''
|
||||
if not value or not re.match(r'^http.?:\/\/[^\s]+$', value):
|
||||
raise ValidationError(
|
||||
_('%(value)s is not a valid remote_id'),
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
|
||||
class ActivitypubFieldMixin:
|
||||
''' make a database field serializable '''
|
||||
def __init__(self, *args, \
|
||||
activitypub_field=None, activitypub_wrapper=None,
|
||||
deduplication_field=False, **kwargs):
|
||||
self.deduplication_field = deduplication_field
|
||||
if activitypub_wrapper:
|
||||
self.activitypub_wrapper = activitypub_field
|
||||
self.activitypub_field = activitypub_wrapper
|
||||
else:
|
||||
self.activitypub_field = activitypub_field
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def field_to_activity(self, value):
|
||||
''' formatter to convert a model value into activitypub '''
|
||||
if hasattr(self, 'activitypub_wrapper'):
|
||||
return {self.activitypub_wrapper: value}
|
||||
return value
|
||||
|
||||
def field_from_activity(self, value):
|
||||
''' formatter to convert activitypub into a model value '''
|
||||
if hasattr(self, 'activitypub_wrapper'):
|
||||
value = value.get(self.activitypub_wrapper)
|
||||
return value
|
||||
|
||||
def get_activitypub_field(self):
|
||||
''' model_field_name to activitypubFieldName '''
|
||||
if self.activitypub_field:
|
||||
return self.activitypub_field
|
||||
name = self.name.split('.')[-1]
|
||||
components = name.split('_')
|
||||
return components[0] + ''.join(x.title() for x in components[1:])
|
||||
|
||||
|
||||
class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
||||
''' default (de)serialization for foreign key and one to one '''
|
||||
def field_from_activity(self, value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
related_model = self.related_model
|
||||
if isinstance(value, dict) and value.get('id'):
|
||||
# this is an activitypub object, which we can deserialize
|
||||
activity_serializer = related_model.activity_serializer
|
||||
return activity_serializer(**value).to_model(related_model)
|
||||
try:
|
||||
# make sure the value looks like a remote id
|
||||
validate_remote_id(value)
|
||||
except ValidationError:
|
||||
# we don't know what this is, ignore it
|
||||
return None
|
||||
# gets or creates the model field from the remote id
|
||||
return activitypub.resolve_remote_id(related_model, value)
|
||||
|
||||
|
||||
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
|
||||
''' a url that serves as a unique identifier '''
|
||||
def __init__(self, *args, max_length=255, validators=None, **kwargs):
|
||||
validators = validators or [validate_remote_id]
|
||||
super().__init__(
|
||||
*args, max_length=max_length, validators=validators,
|
||||
**kwargs
|
||||
)
|
||||
# for this field, the default is true. false everywhere else.
|
||||
self.deduplication_field = kwargs.get('deduplication_field', True)
|
||||
|
||||
|
||||
class UsernameField(ActivitypubFieldMixin, models.CharField):
|
||||
''' activitypub-aware username field '''
|
||||
def __init__(self, activitypub_field='preferredUsername'):
|
||||
self.activitypub_field = activitypub_field
|
||||
# I don't totally know why pylint is mad at this, but it makes it work
|
||||
super( #pylint: disable=bad-super-call
|
||||
ActivitypubFieldMixin, self
|
||||
).__init__(
|
||||
_('username'),
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[AbstractUser.username_validator],
|
||||
error_messages={
|
||||
'unique': _('A user with that username already exists.'),
|
||||
},
|
||||
)
|
||||
|
||||
def deconstruct(self):
|
||||
''' implementation of models.Field deconstruct '''
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
del kwargs['verbose_name']
|
||||
del kwargs['max_length']
|
||||
del kwargs['unique']
|
||||
del kwargs['validators']
|
||||
del kwargs['error_messages']
|
||||
return name, path, args, kwargs
|
||||
|
||||
def field_to_activity(self, value):
|
||||
return value.split('@')[0]
|
||||
|
||||
|
||||
class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
|
||||
''' activitypub-aware foreign key field '''
|
||||
def field_to_activity(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return value.remote_id
|
||||
|
||||
|
||||
class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
|
||||
''' activitypub-aware foreign key field '''
|
||||
def field_to_activity(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return value.to_activity()
|
||||
|
||||
|
||||
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||
''' activitypub-aware many to many field '''
|
||||
def __init__(self, *args, link_only=False, **kwargs):
|
||||
self.link_only = link_only
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def field_to_activity(self, value):
|
||||
if self.link_only:
|
||||
return '%s/%s' % (value.instance.remote_id, self.name)
|
||||
return [i.remote_id for i in value.all()]
|
||||
|
||||
def field_from_activity(self, value):
|
||||
items = []
|
||||
for remote_id in value:
|
||||
try:
|
||||
validate_remote_id(remote_id)
|
||||
except ValidationError:
|
||||
continue
|
||||
items.append(
|
||||
activitypub.resolve_remote_id(self.related_model, remote_id)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
class TagField(ManyToManyField):
|
||||
''' special case of many to many that uses Tags '''
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.activitypub_field = 'tag'
|
||||
|
||||
def field_to_activity(self, value):
|
||||
tags = []
|
||||
for item in value.all():
|
||||
activity_type = item.__class__.__name__
|
||||
if activity_type == 'User':
|
||||
activity_type = 'Mention'
|
||||
tags.append(activitypub.Link(
|
||||
href=item.remote_id,
|
||||
name=getattr(item, item.name_field),
|
||||
type=activity_type
|
||||
))
|
||||
return tags
|
||||
|
||||
def field_from_activity(self, value):
|
||||
if not isinstance(value, list):
|
||||
return None
|
||||
items = []
|
||||
for link_json in value:
|
||||
link = activitypub.Link(**link_json)
|
||||
tag_type = link.type if link.type != 'Mention' else 'Person'
|
||||
if tag_type != self.related_model.activity_serializer.type:
|
||||
# tags can contain multiple types
|
||||
continue
|
||||
items.append(
|
||||
activitypub.resolve_remote_id(self.related_model, link.href)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def image_serializer(value):
|
||||
''' helper for serializing images '''
|
||||
if value and hasattr(value, 'url'):
|
||||
url = value.url
|
||||
else:
|
||||
return None
|
||||
url = 'https://%s%s' % (DOMAIN, url)
|
||||
return activitypub.Image(url=url)
|
||||
|
||||
|
||||
class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||
''' activitypub-aware image field '''
|
||||
def field_to_activity(self, value):
|
||||
return image_serializer(value)
|
||||
|
||||
def field_from_activity(self, value):
|
||||
image_slug = value
|
||||
# when it's an inline image (User avatar/icon, Book cover), it's a json
|
||||
# blob, but when it's an attached image, it's just a url
|
||||
if isinstance(image_slug, dict):
|
||||
url = image_slug.get('url')
|
||||
elif isinstance(image_slug, str):
|
||||
url = image_slug
|
||||
else:
|
||||
return None
|
||||
|
||||
try:
|
||||
validate_remote_id(url)
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
response = get_image(url)
|
||||
if not response:
|
||||
return None
|
||||
|
||||
image_name = str(uuid4()) + '.' + url.split('.')[-1]
|
||||
image_content = ContentFile(response.content)
|
||||
return [image_name, image_content]
|
||||
|
||||
|
||||
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
|
||||
''' activitypub-aware datetime field '''
|
||||
def field_to_activity(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return value.isoformat()
|
||||
|
||||
def field_from_activity(self, value):
|
||||
try:
|
||||
date_value = dateutil.parser.parse(value)
|
||||
try:
|
||||
return timezone.make_aware(date_value)
|
||||
except ValueError:
|
||||
return date_value
|
||||
except (ParserError, TypeError):
|
||||
return None
|
||||
|
||||
class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
|
||||
''' activitypub-aware array field '''
|
||||
def field_to_activity(self, value):
|
||||
return [str(i) for i in value]
|
||||
|
||||
class CharField(ActivitypubFieldMixin, models.CharField):
|
||||
''' activitypub-aware char field '''
|
||||
|
||||
class TextField(ActivitypubFieldMixin, models.TextField):
|
||||
''' activitypub-aware text field '''
|
||||
|
||||
class BooleanField(ActivitypubFieldMixin, models.BooleanField):
|
||||
''' activitypub-aware boolean field '''
|
||||
|
||||
class IntegerField(ActivitypubFieldMixin, models.IntegerField):
|
||||
''' activitypub-aware boolean field '''
|
|
@ -9,6 +9,8 @@ from bookwyrm import books_manager
|
|||
from bookwyrm.connectors import ConnectorException
|
||||
from bookwyrm.models import ReadThrough, User, Book
|
||||
from bookwyrm.utils.fields import JSONField
|
||||
from .base_model import PrivacyLevels
|
||||
|
||||
|
||||
# Mapping goodreads -> bookwyrm shelf titles.
|
||||
GOODREADS_SHELVES = {
|
||||
|
@ -40,8 +42,14 @@ class ImportJob(models.Model):
|
|||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_date = models.DateTimeField(default=timezone.now)
|
||||
task_id = models.CharField(max_length=100, null=True)
|
||||
import_status = models.ForeignKey(
|
||||
'Status', null=True, on_delete=models.PROTECT)
|
||||
include_reviews = models.BooleanField(default=True)
|
||||
privacy = models.CharField(
|
||||
max_length=255,
|
||||
default='public',
|
||||
choices=PrivacyLevels.choices
|
||||
)
|
||||
retry = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class ImportItem(models.Model):
|
||||
''' a single line of a csv being imported '''
|
||||
|
@ -64,13 +72,14 @@ class ImportItem(models.Model):
|
|||
|
||||
def get_book_from_isbn(self):
|
||||
''' search by isbn '''
|
||||
search_result = books_manager.first_search_result(self.isbn)
|
||||
search_result = books_manager.first_search_result(
|
||||
self.isbn, min_confidence=0.999
|
||||
)
|
||||
if search_result:
|
||||
try:
|
||||
# don't crash the import when the connector fails
|
||||
return books_manager.get_or_create_book(search_result.key)
|
||||
except ConnectorException:
|
||||
pass
|
||||
# raises ConnectorException
|
||||
return books_manager.get_or_create_book(search_result.key)
|
||||
return None
|
||||
|
||||
|
||||
def get_book_from_title_author(self):
|
||||
''' search by title and author '''
|
||||
|
@ -78,9 +87,24 @@ class ImportItem(models.Model):
|
|||
self.data['Title'],
|
||||
self.data['Author']
|
||||
)
|
||||
search_result = books_manager.first_search_result(search_term)
|
||||
search_result = books_manager.first_search_result(
|
||||
search_term, min_confidence=0.999
|
||||
)
|
||||
if search_result:
|
||||
# raises ConnectorException
|
||||
return books_manager.get_or_create_book(search_result.key)
|
||||
return None
|
||||
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
''' get the book title '''
|
||||
return self.data['Title']
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
''' get the book title '''
|
||||
return self.data['Author']
|
||||
|
||||
@property
|
||||
def isbn(self):
|
||||
|
@ -92,6 +116,7 @@ class ImportItem(models.Model):
|
|||
''' the goodreads shelf field '''
|
||||
if self.data['Exclusive Shelf']:
|
||||
return GOODREADS_SHELVES.get(self.data['Exclusive Shelf'])
|
||||
return None
|
||||
|
||||
@property
|
||||
def review(self):
|
||||
|
@ -107,13 +132,17 @@ class ImportItem(models.Model):
|
|||
def date_added(self):
|
||||
''' when the book was added to this dataset '''
|
||||
if self.data['Date Added']:
|
||||
return dateutil.parser.parse(self.data['Date Added'])
|
||||
return timezone.make_aware(
|
||||
dateutil.parser.parse(self.data['Date Added']))
|
||||
return None
|
||||
|
||||
@property
|
||||
def date_read(self):
|
||||
''' the date a book was completed '''
|
||||
if self.data['Date Read']:
|
||||
return dateutil.parser.parse(self.data['Date Read'])
|
||||
return timezone.make_aware(
|
||||
dateutil.parser.parse(self.data['Date Read']))
|
||||
return None
|
||||
|
||||
@property
|
||||
def reads(self):
|
||||
|
@ -123,6 +152,7 @@ class ImportItem(models.Model):
|
|||
return [ReadThrough(start_date=self.date_added)]
|
||||
if self.date_read:
|
||||
return [ReadThrough(
|
||||
start_date=self.date_added,
|
||||
finish_date=self.date_read,
|
||||
)]
|
||||
return []
|
||||
|
|
|
@ -2,23 +2,24 @@
|
|||
from django.db import models
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import BookWyrmModel
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
class UserRelationship(BookWyrmModel):
|
||||
class UserRelationship(ActivitypubMixin, BookWyrmModel):
|
||||
''' many-to-many through table for followers '''
|
||||
user_subject = models.ForeignKey(
|
||||
user_subject = fields.ForeignKey(
|
||||
'User',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='%(class)s_user_subject'
|
||||
related_name='%(class)s_user_subject',
|
||||
activitypub_field='actor',
|
||||
)
|
||||
user_object = models.ForeignKey(
|
||||
user_object = fields.ForeignKey(
|
||||
'User',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='%(class)s_user_object'
|
||||
related_name='%(class)s_user_object',
|
||||
activitypub_field='object',
|
||||
)
|
||||
# follow or follow_request for pending TODO: blocking?
|
||||
relationship_id = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
''' relationships should be unique '''
|
||||
|
@ -34,25 +35,30 @@ class UserRelationship(BookWyrmModel):
|
|||
)
|
||||
]
|
||||
|
||||
def get_remote_id(self):
|
||||
activity_serializer = activitypub.Follow
|
||||
|
||||
def get_remote_id(self, status=None):
|
||||
''' use shelf identifier in remote_id '''
|
||||
status = status or 'follows'
|
||||
base_path = self.user_subject.remote_id
|
||||
return '%s#%s/%d' % (base_path, self.status, self.id)
|
||||
return '%s#%s/%d' % (base_path, status, self.id)
|
||||
|
||||
|
||||
def to_accept_activity(self):
|
||||
''' generate an Accept for this follow request '''
|
||||
return activitypub.Accept(
|
||||
id='%s#accepts/follows/' % self.remote_id,
|
||||
actor=self.user_subject.remote_id,
|
||||
object=self.user_object.remote_id,
|
||||
id=self.get_remote_id(status='accepts'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_reject_activity(self):
|
||||
''' generate an Accept for this follow request '''
|
||||
return activitypub.Reject(
|
||||
id='%s#rejects/follows/' % self.remote_id,
|
||||
actor=self.user_subject.remote_id,
|
||||
object=self.user_object.remote_id,
|
||||
id=self.get_remote_id(status='rejects'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
|
||||
|
||||
|
@ -66,7 +72,7 @@ class UserFollows(UserRelationship):
|
|||
return cls(
|
||||
user_subject=follow_request.user_subject,
|
||||
user_object=follow_request.user_object,
|
||||
relationship_id=follow_request.relationship_id,
|
||||
remote_id=follow_request.remote_id,
|
||||
)
|
||||
|
||||
|
||||
|
@ -74,13 +80,16 @@ class UserFollowRequest(UserRelationship):
|
|||
''' following a user requires manual or automatic confirmation '''
|
||||
status = 'follow_request'
|
||||
|
||||
def to_activity(self):
|
||||
''' request activity '''
|
||||
return activitypub.Follow(
|
||||
id=self.remote_id,
|
||||
actor=self.user_subject.remote_id,
|
||||
object=self.user_object.remote_id,
|
||||
).serialize()
|
||||
def save(self, *args, **kwargs):
|
||||
''' make sure the follow relationship doesn't already exist '''
|
||||
try:
|
||||
UserFollows.objects.get(
|
||||
user_subject=self.user_subject,
|
||||
user_object=self.user_object
|
||||
)
|
||||
return None
|
||||
except UserFollows.DoesNotExist:
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class UserBlocks(UserRelationship):
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
''' puttin' books on shelves '''
|
||||
import re
|
||||
from django.db import models
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import BookWyrmModel, OrderedCollectionMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from .base_model import OrderedCollectionMixin, PrivacyLevels
|
||||
from . import fields
|
||||
|
||||
|
||||
class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||
''' a list of books owned by a user '''
|
||||
name = models.CharField(max_length=100)
|
||||
name = fields.CharField(max_length=100)
|
||||
identifier = models.CharField(max_length=100)
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
user = fields.ForeignKey(
|
||||
'User', on_delete=models.PROTECT, activitypub_field='owner')
|
||||
editable = models.BooleanField(default=True)
|
||||
privacy = fields.CharField(
|
||||
max_length=255,
|
||||
default='public',
|
||||
choices=PrivacyLevels.choices
|
||||
)
|
||||
books = models.ManyToManyField(
|
||||
'Edition',
|
||||
symmetrical=False,
|
||||
|
@ -18,6 +27,15 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
through_fields=('shelf', 'book')
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' set the identifier '''
|
||||
saved = super().save(*args, **kwargs)
|
||||
if not self.identifier:
|
||||
slug = re.sub(r'[^\w]', '', self.name).lower()
|
||||
self.identifier = '%s-%d' % (slug, self.id)
|
||||
return super().save(*args, **kwargs)
|
||||
return saved
|
||||
|
||||
@property
|
||||
def collection_queryset(self):
|
||||
''' list of books for this shelf, overrides OrderedCollectionMixin '''
|
||||
|
@ -35,22 +53,27 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
|
||||
class ShelfBook(BookWyrmModel):
|
||||
''' many to many join table for books and shelves '''
|
||||
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
||||
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
|
||||
added_by = models.ForeignKey(
|
||||
book = fields.ForeignKey(
|
||||
'Edition', on_delete=models.PROTECT, activitypub_field='object')
|
||||
shelf = fields.ForeignKey(
|
||||
'Shelf', on_delete=models.PROTECT, activitypub_field='target')
|
||||
added_by = fields.ForeignKey(
|
||||
'User',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.PROTECT
|
||||
on_delete=models.PROTECT,
|
||||
activitypub_field='actor'
|
||||
)
|
||||
|
||||
activity_serializer = activitypub.AddBook
|
||||
|
||||
def to_add_activity(self, user):
|
||||
''' AP for shelving a book'''
|
||||
return activitypub.Add(
|
||||
id='%s#add' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.book.to_activity(),
|
||||
target=self.shelf.to_activity()
|
||||
target=self.shelf.remote_id,
|
||||
).serialize()
|
||||
|
||||
def to_remove_activity(self, user):
|
||||
|
|
|
@ -29,6 +29,9 @@ class SiteSettings(models.Model):
|
|||
upload_to='static/images/',
|
||||
default='/static/images/favicon.ico'
|
||||
)
|
||||
support_link = models.CharField(max_length=255, null=True, blank=True)
|
||||
support_title = models.CharField(max_length=100, null=True, blank=True)
|
||||
admin_email = models.EmailField(max_length=255, null=True, blank=True)
|
||||
|
||||
@classmethod
|
||||
def get(cls):
|
||||
|
@ -66,7 +69,7 @@ class SiteInvite(models.Model):
|
|||
|
||||
def get_passowrd_reset_expiry():
|
||||
''' give people a limited time to use the link '''
|
||||
now = datetime.datetime.now()
|
||||
now = timezone.now()
|
||||
return now + datetime.timedelta(days=1)
|
||||
|
||||
|
||||
|
|
|
@ -1,27 +1,34 @@
|
|||
''' models for storing different kinds of Activities '''
|
||||
from django.utils import timezone
|
||||
from django.utils.http import http_date
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from model_utils.managers import InheritanceManager
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
|
||||
from .base_model import ActivityMapping, BookWyrmModel
|
||||
|
||||
from .base_model import BookWyrmModel, PrivacyLevels
|
||||
from . import fields
|
||||
from .fields import image_serializer
|
||||
|
||||
class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
''' any post, like a reply to a review, etc '''
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
content = models.TextField(blank=True, null=True)
|
||||
mention_users = models.ManyToManyField('User', related_name='mention_user')
|
||||
mention_books = models.ManyToManyField(
|
||||
'Edition', related_name='mention_book')
|
||||
user = fields.ForeignKey(
|
||||
'User', on_delete=models.PROTECT, activitypub_field='attributedTo')
|
||||
content = fields.TextField(blank=True, null=True)
|
||||
mention_users = fields.TagField('User', related_name='mention_user')
|
||||
mention_books = fields.TagField('Edition', related_name='mention_book')
|
||||
local = models.BooleanField(default=True)
|
||||
privacy = models.CharField(max_length=255, default='public')
|
||||
sensitive = models.BooleanField(default=False)
|
||||
privacy = models.CharField(
|
||||
max_length=255,
|
||||
default='public',
|
||||
choices=PrivacyLevels.choices
|
||||
)
|
||||
sensitive = fields.BooleanField(default=False)
|
||||
# the created date can't be this, because of receiving federated posts
|
||||
published_date = models.DateTimeField(default=timezone.now)
|
||||
published_date = fields.DateTimeField(
|
||||
default=timezone.now, activitypub_field='published')
|
||||
deleted = models.BooleanField(default=False)
|
||||
deleted_date = models.DateTimeField(blank=True, null=True)
|
||||
favorites = models.ManyToManyField(
|
||||
'User',
|
||||
symmetrical=False,
|
||||
|
@ -29,60 +36,17 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
through_fields=('status', 'user'),
|
||||
related_name='user_favorites'
|
||||
)
|
||||
reply_parent = models.ForeignKey(
|
||||
reply_parent = fields.ForeignKey(
|
||||
'self',
|
||||
null=True,
|
||||
on_delete=models.PROTECT
|
||||
on_delete=models.PROTECT,
|
||||
activitypub_field='inReplyTo',
|
||||
)
|
||||
objects = InheritanceManager()
|
||||
|
||||
# ---- activitypub serialization settings for this model ----- #
|
||||
@property
|
||||
def ap_to(self):
|
||||
''' should be related to post privacy I think '''
|
||||
return ['https://www.w3.org/ns/activitystreams#Public']
|
||||
|
||||
@property
|
||||
def ap_cc(self):
|
||||
''' should be related to post privacy I think '''
|
||||
return [self.user.ap_followers]
|
||||
|
||||
@property
|
||||
def ap_replies(self):
|
||||
''' structured replies block '''
|
||||
return self.to_replies()
|
||||
|
||||
shared_mappings = [
|
||||
ActivityMapping('id', 'remote_id'),
|
||||
ActivityMapping('url', 'remote_id'),
|
||||
ActivityMapping('inReplyTo', 'reply_parent'),
|
||||
ActivityMapping(
|
||||
'published',
|
||||
'published_date',
|
||||
activity_formatter=lambda d: http_date(d.timestamp())
|
||||
),
|
||||
ActivityMapping('attributedTo', 'user'),
|
||||
ActivityMapping('to', 'ap_to'),
|
||||
ActivityMapping('cc', 'ap_cc'),
|
||||
ActivityMapping('replies', 'ap_replies'),
|
||||
]
|
||||
|
||||
# serializing to bookwyrm expanded activitypub
|
||||
activity_mappings = shared_mappings + [
|
||||
ActivityMapping('name', 'name'),
|
||||
ActivityMapping('inReplyToBook', 'book'),
|
||||
ActivityMapping('rating', 'rating'),
|
||||
ActivityMapping('quote', 'quote'),
|
||||
ActivityMapping('content', 'content'),
|
||||
]
|
||||
|
||||
# for serializing to standard activitypub without extended types
|
||||
pure_activity_mappings = shared_mappings + [
|
||||
ActivityMapping('name', 'ap_pure_name'),
|
||||
ActivityMapping('content', 'ap_pure_content'),
|
||||
]
|
||||
|
||||
activity_serializer = activitypub.Note
|
||||
serialize_reverse_fields = [('attachments', 'attachment')]
|
||||
deserialize_reverse_fields = [('attachments', 'attachment')]
|
||||
|
||||
#----- replies collection activitypub ----#
|
||||
@classmethod
|
||||
|
@ -104,60 +68,118 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
**kwargs
|
||||
)
|
||||
|
||||
class GeneratedStatus(Status):
|
||||
def to_activity(self, pure=False):
|
||||
''' return tombstone if the status is deleted '''
|
||||
if self.deleted:
|
||||
return activitypub.Tombstone(
|
||||
id=self.remote_id,
|
||||
url=self.remote_id,
|
||||
deleted=self.deleted_date.isoformat(),
|
||||
published=self.deleted_date.isoformat()
|
||||
).serialize()
|
||||
activity = ActivitypubMixin.to_activity(self)
|
||||
activity['replies'] = self.to_replies()
|
||||
|
||||
# privacy controls
|
||||
public = 'https://www.w3.org/ns/activitystreams#Public'
|
||||
mentions = [u.remote_id for u in self.mention_users.all()]
|
||||
# this is a link to the followers list:
|
||||
followers = self.user.__class__._meta.get_field('followers')\
|
||||
.field_to_activity(self.user.followers)
|
||||
if self.privacy == 'public':
|
||||
activity['to'] = [public]
|
||||
activity['cc'] = [followers] + mentions
|
||||
elif self.privacy == 'unlisted':
|
||||
activity['to'] = [followers]
|
||||
activity['cc'] = [public] + mentions
|
||||
elif self.privacy == 'followers':
|
||||
activity['to'] = [followers]
|
||||
activity['cc'] = mentions
|
||||
if self.privacy == 'direct':
|
||||
activity['to'] = mentions
|
||||
activity['cc'] = []
|
||||
|
||||
# "pure" serialization for non-bookwyrm instances
|
||||
if pure:
|
||||
activity['content'] = self.pure_content
|
||||
if 'name' in activity:
|
||||
activity['name'] = self.pure_name
|
||||
activity['type'] = self.pure_type
|
||||
activity['attachment'] = [
|
||||
image_serializer(b.cover) for b in self.mention_books.all() \
|
||||
if b.cover]
|
||||
if hasattr(self, 'book'):
|
||||
activity['attachment'].append(
|
||||
image_serializer(self.book.cover)
|
||||
)
|
||||
return activity
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' update user active time '''
|
||||
if self.user.local:
|
||||
self.user.last_active_date = timezone.now()
|
||||
self.user.save()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class GeneratedNote(Status):
|
||||
''' these are app-generated messages about user activity '''
|
||||
@property
|
||||
def ap_pure_content(self):
|
||||
def pure_content(self):
|
||||
''' indicate the book in question for mastodon (or w/e) users '''
|
||||
message = self.content
|
||||
books = ', '.join(
|
||||
'<a href="%s">"%s"</a>' % (self.book.local_id, self.book.title) \
|
||||
for book in self.mention_books
|
||||
'<a href="%s">"%s"</a>' % (book.remote_id, book.title) \
|
||||
for book in self.mention_books.all()
|
||||
)
|
||||
return '%s %s' % (message, books)
|
||||
return '%s %s %s' % (self.user.display_name, message, books)
|
||||
|
||||
activity_serializer = activitypub.GeneratedNote
|
||||
pure_activity_serializer = activitypub.Note
|
||||
pure_type = 'Note'
|
||||
|
||||
|
||||
class Comment(Status):
|
||||
''' like a review but without a rating and transient '''
|
||||
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
||||
book = fields.ForeignKey(
|
||||
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
|
||||
|
||||
@property
|
||||
def ap_pure_content(self):
|
||||
def pure_content(self):
|
||||
''' indicate the book in question for mastodon (or w/e) users '''
|
||||
return self.content + '<br><br>(comment on <a href="%s">"%s"</a>)' % \
|
||||
(self.book.local_id, self.book.title)
|
||||
(self.book.remote_id, self.book.title)
|
||||
|
||||
activity_serializer = activitypub.Comment
|
||||
pure_activity_serializer = activitypub.Note
|
||||
pure_type = 'Note'
|
||||
|
||||
|
||||
class Quotation(Status):
|
||||
''' like a review but without a rating and transient '''
|
||||
quote = models.TextField()
|
||||
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
||||
quote = fields.TextField()
|
||||
book = fields.ForeignKey(
|
||||
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
|
||||
|
||||
@property
|
||||
def ap_pure_content(self):
|
||||
def pure_content(self):
|
||||
''' indicate the book in question for mastodon (or w/e) users '''
|
||||
return '"%s"<br>-- <a href="%s">"%s"</a>)<br><br>%s' % (
|
||||
return '"%s"<br>-- <a href="%s">"%s"</a><br><br>%s' % (
|
||||
self.quote,
|
||||
self.book.local_id,
|
||||
self.book.remote_id,
|
||||
self.book.title,
|
||||
self.content,
|
||||
)
|
||||
|
||||
activity_serializer = activitypub.Quotation
|
||||
pure_activity_serializer = activitypub.Note
|
||||
pure_type = 'Note'
|
||||
|
||||
|
||||
class Review(Status):
|
||||
''' a book review '''
|
||||
name = models.CharField(max_length=255, null=True)
|
||||
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
||||
rating = models.IntegerField(
|
||||
name = fields.CharField(max_length=255, null=True)
|
||||
book = fields.ForeignKey(
|
||||
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
|
||||
rating = fields.IntegerField(
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -165,38 +187,43 @@ class Review(Status):
|
|||
)
|
||||
|
||||
@property
|
||||
def ap_pure_name(self):
|
||||
def pure_name(self):
|
||||
''' clarify review names for mastodon serialization '''
|
||||
return 'Review of "%s" (%d stars): %s' % (
|
||||
if self.rating:
|
||||
return 'Review of "%s" (%d stars): %s' % (
|
||||
self.book.title,
|
||||
self.rating,
|
||||
self.name
|
||||
)
|
||||
return 'Review of "%s": %s' % (
|
||||
self.book.title,
|
||||
self.rating,
|
||||
self.name
|
||||
)
|
||||
|
||||
@property
|
||||
def ap_pure_content(self):
|
||||
def pure_content(self):
|
||||
''' indicate the book in question for mastodon (or w/e) users '''
|
||||
return self.content + '<br><br>(<a href="%s">"%s"</a>)' % \
|
||||
(self.book.local_id, self.book.title)
|
||||
(self.book.remote_id, self.book.title)
|
||||
|
||||
activity_serializer = activitypub.Review
|
||||
pure_activity_serializer = activitypub.Article
|
||||
pure_type = 'Article'
|
||||
|
||||
|
||||
class Favorite(ActivitypubMixin, BookWyrmModel):
|
||||
''' fav'ing a post '''
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
status = models.ForeignKey('Status', on_delete=models.PROTECT)
|
||||
|
||||
# ---- activitypub serialization settings for this model ----- #
|
||||
activity_mappings = [
|
||||
ActivityMapping('id', 'remote_id'),
|
||||
ActivityMapping('actor', 'user'),
|
||||
ActivityMapping('object', 'status'),
|
||||
]
|
||||
user = fields.ForeignKey(
|
||||
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
||||
status = fields.ForeignKey(
|
||||
'Status', on_delete=models.PROTECT, activitypub_field='object')
|
||||
|
||||
activity_serializer = activitypub.Like
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' update user active time '''
|
||||
self.user.last_active_date = timezone.now()
|
||||
self.user.save()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
''' can't fav things twice '''
|
||||
|
@ -205,16 +232,12 @@ class Favorite(ActivitypubMixin, BookWyrmModel):
|
|||
|
||||
class Boost(Status):
|
||||
''' boost'ing a post '''
|
||||
boosted_status = models.ForeignKey(
|
||||
boosted_status = fields.ForeignKey(
|
||||
'Status',
|
||||
on_delete=models.PROTECT,
|
||||
related_name="boosters")
|
||||
|
||||
activity_mappings = [
|
||||
ActivityMapping('id', 'remote_id'),
|
||||
ActivityMapping('actor', 'user'),
|
||||
ActivityMapping('object', 'boosted_status'),
|
||||
]
|
||||
related_name='boosters',
|
||||
activitypub_field='object',
|
||||
)
|
||||
|
||||
activity_serializer = activitypub.Boost
|
||||
|
||||
|
@ -237,10 +260,16 @@ class ReadThrough(BookWyrmModel):
|
|||
blank=True,
|
||||
null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' update user active time '''
|
||||
self.user.last_active_date = timezone.now()
|
||||
self.user.save()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
NotificationType = models.TextChoices(
|
||||
'NotificationType',
|
||||
'FAVORITE REPLY TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
|
||||
'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
|
||||
|
||||
class Notification(BookWyrmModel):
|
||||
''' you've been tagged, liked, followed, etc '''
|
||||
|
|
|
@ -6,13 +6,12 @@ from django.db import models
|
|||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .base_model import OrderedCollectionMixin, BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
class Tag(OrderedCollectionMixin, BookWyrmModel):
|
||||
''' freeform tags for books '''
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
||||
name = models.CharField(max_length=100)
|
||||
name = fields.CharField(max_length=100, unique=True)
|
||||
identifier = models.CharField(max_length=100)
|
||||
|
||||
@classmethod
|
||||
|
@ -30,13 +29,33 @@ class Tag(OrderedCollectionMixin, BookWyrmModel):
|
|||
base_path = 'https://%s' % DOMAIN
|
||||
return '%s/tag/%s' % (base_path, self.identifier)
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' create a url-safe lookup key for the tag '''
|
||||
if not self.id:
|
||||
# add identifiers to new tags
|
||||
self.identifier = urllib.parse.quote_plus(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class UserTag(BookWyrmModel):
|
||||
''' an instance of a tag on a book by a user '''
|
||||
user = fields.ForeignKey(
|
||||
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
||||
book = fields.ForeignKey(
|
||||
'Edition', on_delete=models.PROTECT, activitypub_field='object')
|
||||
tag = fields.ForeignKey(
|
||||
'Tag', on_delete=models.PROTECT, activitypub_field='target')
|
||||
|
||||
activity_serializer = activitypub.AddBook
|
||||
|
||||
def to_add_activity(self, user):
|
||||
''' AP for shelving a book'''
|
||||
return activitypub.Add(
|
||||
id='%s#add' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.book.to_activity(),
|
||||
target=self.to_activity(),
|
||||
target=self.remote_id,
|
||||
).serialize()
|
||||
|
||||
def to_remove_activity(self, user):
|
||||
|
@ -48,13 +67,7 @@ class Tag(OrderedCollectionMixin, BookWyrmModel):
|
|||
target=self.to_activity(),
|
||||
).serialize()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' create a url-safe lookup key for the tag '''
|
||||
if not self.id:
|
||||
# add identifiers to new tags
|
||||
self.identifier = urllib.parse.quote_plus(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
''' unqiueness constraint '''
|
||||
unique_together = ('user', 'book', 'name')
|
||||
unique_together = ('user', 'book', 'tag')
|
||||
|
|
|
@ -6,43 +6,61 @@ from django.db import models
|
|||
from django.dispatch import receiver
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.connectors import get_data
|
||||
from bookwyrm.models.shelf import Shelf
|
||||
from bookwyrm.models.status import Status
|
||||
from bookwyrm.models.status import Status, Review
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.signatures import create_key_pair
|
||||
from bookwyrm.tasks import app
|
||||
from .base_model import OrderedCollectionPageMixin
|
||||
from .base_model import ActivityMapping
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from .federated_server import FederatedServer
|
||||
from . import fields
|
||||
|
||||
|
||||
class User(OrderedCollectionPageMixin, AbstractUser):
|
||||
''' a user who wants to read books '''
|
||||
private_key = models.TextField(blank=True, null=True)
|
||||
public_key = models.TextField(blank=True, null=True)
|
||||
inbox = models.CharField(max_length=255, unique=True)
|
||||
shared_inbox = models.CharField(max_length=255, blank=True, null=True)
|
||||
username = fields.UsernameField()
|
||||
|
||||
key_pair = fields.OneToOneField(
|
||||
'KeyPair',
|
||||
on_delete=models.CASCADE,
|
||||
blank=True, null=True,
|
||||
activitypub_field='publicKey',
|
||||
related_name='owner'
|
||||
)
|
||||
inbox = fields.RemoteIdField(unique=True)
|
||||
shared_inbox = fields.RemoteIdField(
|
||||
activitypub_field='sharedInbox',
|
||||
activitypub_wrapper='endpoints',
|
||||
deduplication_field=False,
|
||||
null=True)
|
||||
federated_server = models.ForeignKey(
|
||||
'FederatedServer',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
outbox = models.CharField(max_length=255, unique=True)
|
||||
summary = models.TextField(blank=True, null=True)
|
||||
local = models.BooleanField(default=True)
|
||||
bookwyrm_user = models.BooleanField(default=True)
|
||||
outbox = fields.RemoteIdField(unique=True)
|
||||
summary = fields.TextField(default='')
|
||||
local = models.BooleanField(default=False)
|
||||
bookwyrm_user = fields.BooleanField(default=True)
|
||||
localname = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
unique=True
|
||||
)
|
||||
# name is your display name, which you can change at will
|
||||
name = models.CharField(max_length=100, blank=True, null=True)
|
||||
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||
following = models.ManyToManyField(
|
||||
name = fields.CharField(max_length=100, default='')
|
||||
avatar = fields.ImageField(
|
||||
upload_to='avatars/', blank=True, null=True, activitypub_field='icon')
|
||||
followers = fields.ManyToManyField(
|
||||
'self',
|
||||
link_only=True,
|
||||
symmetrical=False,
|
||||
through='UserFollows',
|
||||
through_fields=('user_subject', 'user_object'),
|
||||
related_name='followers'
|
||||
through_fields=('user_object', 'user_subject'),
|
||||
related_name='following'
|
||||
)
|
||||
follow_requests = models.ManyToManyField(
|
||||
'self',
|
||||
|
@ -65,93 +83,44 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
through_fields=('user', 'status'),
|
||||
related_name='favorite_statuses'
|
||||
)
|
||||
remote_id = models.CharField(max_length=255, null=True, unique=True)
|
||||
remote_id = fields.RemoteIdField(
|
||||
null=True, unique=True, activitypub_field='id')
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
updated_date = models.DateTimeField(auto_now=True)
|
||||
manually_approves_followers = models.BooleanField(default=False)
|
||||
|
||||
# ---- activitypub serialization settings for this model ----- #
|
||||
@property
|
||||
def ap_followers(self):
|
||||
''' generates url for activitypub followers page '''
|
||||
return '%s/followers' % self.remote_id
|
||||
last_active_date = models.DateTimeField(auto_now=True)
|
||||
manually_approves_followers = fields.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def ap_icon(self):
|
||||
''' send default icon if one isn't set '''
|
||||
if self.avatar:
|
||||
url = self.avatar.url
|
||||
# TODO not the right way to get the media type
|
||||
media_type = 'image/%s' % url.split('.')[-1]
|
||||
else:
|
||||
url = 'https://%s/static/images/default_avi.jpg' % DOMAIN
|
||||
media_type = 'image/jpeg'
|
||||
return activitypub.Image(media_type, url, 'Image')
|
||||
def display_name(self):
|
||||
''' show the cleanest version of the user's name possible '''
|
||||
if self.name != '':
|
||||
return self.name
|
||||
return self.localname or self.username
|
||||
|
||||
@property
|
||||
def ap_public_key(self):
|
||||
''' format the public key block for activitypub '''
|
||||
return activitypub.PublicKey(**{
|
||||
'id': '%s/#main-key' % self.remote_id,
|
||||
'owner': self.remote_id,
|
||||
'publicKeyPem': self.public_key,
|
||||
})
|
||||
|
||||
activity_mappings = [
|
||||
ActivityMapping('id', 'remote_id'),
|
||||
ActivityMapping(
|
||||
'preferredUsername',
|
||||
'username',
|
||||
activity_formatter=lambda x: x.split('@')[0]
|
||||
),
|
||||
ActivityMapping('name', 'name'),
|
||||
ActivityMapping('inbox', 'inbox'),
|
||||
ActivityMapping('outbox', 'outbox'),
|
||||
ActivityMapping('followers', 'ap_followers'),
|
||||
ActivityMapping('summary', 'summary'),
|
||||
ActivityMapping(
|
||||
'publicKey',
|
||||
'public_key',
|
||||
model_formatter=lambda x: x.get('publicKeyPem')
|
||||
),
|
||||
ActivityMapping('publicKey', 'ap_public_key'),
|
||||
ActivityMapping(
|
||||
'endpoints',
|
||||
'shared_inbox',
|
||||
activity_formatter=lambda x: {'sharedInbox': x},
|
||||
model_formatter=lambda x: x.get('sharedInbox')
|
||||
),
|
||||
ActivityMapping('icon', 'ap_icon'),
|
||||
ActivityMapping(
|
||||
'manuallyApprovesFollowers',
|
||||
'manually_approves_followers'
|
||||
),
|
||||
# this field isn't in the activity but should always be false
|
||||
ActivityMapping(None, 'local', model_formatter=lambda x: False),
|
||||
]
|
||||
activity_serializer = activitypub.Person
|
||||
|
||||
def to_outbox(self, **kwargs):
|
||||
''' an ordered collection of statuses '''
|
||||
queryset = Status.objects.filter(
|
||||
user=self,
|
||||
).select_subclasses()
|
||||
deleted=False,
|
||||
).select_subclasses().order_by('-published_date')
|
||||
return self.to_ordered_collection(queryset, \
|
||||
remote_id=self.outbox, **kwargs)
|
||||
|
||||
def to_following_activity(self, **kwargs):
|
||||
''' activitypub following list '''
|
||||
remote_id = '%s/following' % self.remote_id
|
||||
return self.to_ordered_collection(self.following, \
|
||||
return self.to_ordered_collection(self.following.all(), \
|
||||
remote_id=remote_id, id_only=True, **kwargs)
|
||||
|
||||
def to_followers_activity(self, **kwargs):
|
||||
''' activitypub followers list '''
|
||||
remote_id = '%s/followers' % self.remote_id
|
||||
return self.to_ordered_collection(self.followers, \
|
||||
return self.to_ordered_collection(self.followers.all(), \
|
||||
remote_id=remote_id, id_only=True, **kwargs)
|
||||
|
||||
def to_activity(self, pure=False):
|
||||
def to_activity(self):
|
||||
''' override default AP serializer to add context object
|
||||
idk if this is the best way to go about this '''
|
||||
activity_object = super().to_activity()
|
||||
|
@ -168,36 +137,73 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
return activity_object
|
||||
|
||||
|
||||
@receiver(models.signals.pre_save, sender=User)
|
||||
def execute_before_save(sender, instance, *args, **kwargs):
|
||||
''' populate fields for new local users '''
|
||||
# this user already exists, no need to poplate fields
|
||||
if instance.id:
|
||||
return
|
||||
if not instance.local:
|
||||
# we need to generate a username that uses the domain (webfinger format)
|
||||
actor_parts = urlparse(instance.remote_id)
|
||||
instance.username = '%s@%s' % (instance.username, actor_parts.netloc)
|
||||
return
|
||||
def save(self, *args, **kwargs):
|
||||
''' populate fields for new local users '''
|
||||
# this user already exists, no need to populate fields
|
||||
if self.id:
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
# populate fields for local users
|
||||
instance.remote_id = 'https://%s/user/%s' % (DOMAIN, instance.username)
|
||||
instance.localname = instance.username
|
||||
instance.username = '%s@%s' % (instance.username, DOMAIN)
|
||||
instance.actor = instance.remote_id
|
||||
instance.inbox = '%s/inbox' % instance.remote_id
|
||||
instance.shared_inbox = 'https://%s/inbox' % DOMAIN
|
||||
instance.outbox = '%s/outbox' % instance.remote_id
|
||||
if not instance.private_key:
|
||||
instance.private_key, instance.public_key = create_key_pair()
|
||||
if not self.local:
|
||||
# generate a username that uses the domain (webfinger format)
|
||||
actor_parts = urlparse(self.remote_id)
|
||||
self.username = '%s@%s' % (self.username, actor_parts.netloc)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
# populate fields for local users
|
||||
self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.username)
|
||||
self.localname = self.username
|
||||
self.username = '%s@%s' % (self.username, DOMAIN)
|
||||
self.actor = self.remote_id
|
||||
self.inbox = '%s/inbox' % self.remote_id
|
||||
self.shared_inbox = 'https://%s/inbox' % DOMAIN
|
||||
self.outbox = '%s/outbox' % self.remote_id
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class KeyPair(ActivitypubMixin, BookWyrmModel):
|
||||
''' public and private keys for a user '''
|
||||
private_key = models.TextField(blank=True, null=True)
|
||||
public_key = fields.TextField(
|
||||
blank=True, null=True, activitypub_field='publicKeyPem')
|
||||
|
||||
activity_serializer = activitypub.PublicKey
|
||||
serialize_reverse_fields = [('owner', 'owner')]
|
||||
|
||||
def get_remote_id(self):
|
||||
# self.owner is set by the OneToOneField on User
|
||||
return '%s/#main-key' % self.owner.remote_id
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' create a key pair '''
|
||||
if not self.public_key:
|
||||
self.private_key, self.public_key = create_key_pair()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def to_activity(self):
|
||||
''' override default AP serializer to add context object
|
||||
idk if this is the best way to go about this '''
|
||||
activity_object = super().to_activity()
|
||||
del activity_object['@context']
|
||||
del activity_object['type']
|
||||
return activity_object
|
||||
|
||||
|
||||
@receiver(models.signals.post_save, sender=User)
|
||||
#pylint: disable=unused-argument
|
||||
def execute_after_save(sender, instance, created, *args, **kwargs):
|
||||
''' create shelves for new users '''
|
||||
if not instance.local or not created:
|
||||
if not created:
|
||||
return
|
||||
|
||||
if not instance.local:
|
||||
set_remote_server.delay(instance.id)
|
||||
return
|
||||
|
||||
instance.key_pair = KeyPair.objects.create(
|
||||
remote_id='%s/#main-key' % instance.remote_id)
|
||||
instance.save()
|
||||
|
||||
shelves = [{
|
||||
'name': 'To Read',
|
||||
'identifier': 'to-read',
|
||||
|
@ -216,3 +222,54 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
|||
user=instance,
|
||||
editable=False
|
||||
).save()
|
||||
|
||||
|
||||
@app.task
|
||||
def set_remote_server(user_id):
|
||||
''' figure out the user's remote server in the background '''
|
||||
user = User.objects.get(id=user_id)
|
||||
actor_parts = urlparse(user.remote_id)
|
||||
user.federated_server = \
|
||||
get_or_create_remote_server(actor_parts.netloc)
|
||||
user.save()
|
||||
if user.bookwyrm_user:
|
||||
get_remote_reviews.delay(user.outbox)
|
||||
|
||||
|
||||
def get_or_create_remote_server(domain):
|
||||
''' get info on a remote server '''
|
||||
try:
|
||||
return FederatedServer.objects.get(
|
||||
server_name=domain
|
||||
)
|
||||
except FederatedServer.DoesNotExist:
|
||||
pass
|
||||
|
||||
data = get_data('https://%s/.well-known/nodeinfo' % domain)
|
||||
|
||||
try:
|
||||
nodeinfo_url = data.get('links')[0].get('href')
|
||||
except (TypeError, KeyError):
|
||||
return None
|
||||
|
||||
data = get_data(nodeinfo_url)
|
||||
|
||||
server = FederatedServer.objects.create(
|
||||
server_name=domain,
|
||||
application_type=data['software']['name'],
|
||||
application_version=data['software']['version'],
|
||||
)
|
||||
return server
|
||||
|
||||
|
||||
@app.task
|
||||
def get_remote_reviews(outbox):
|
||||
''' ingest reviews by a new remote bookwyrm user '''
|
||||
outbox_page = outbox + '?page=true'
|
||||
data = get_data(outbox_page)
|
||||
|
||||
# TODO: pagination?
|
||||
for activity in data['orderedItems']:
|
||||
if not activity['type'] == 'Review':
|
||||
continue
|
||||
activitypub.Review(**activity).to_model(Review)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue