forked from mirrors/bookwyrm
Merge branch 'main' into review-rate
This commit is contained in:
commit
ad43e5c83a
293 changed files with 19710 additions and 9166 deletions
13
.github/workflows/black.yml
vendored
Normal file
13
.github/workflows/black.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- uses: psf/black@stable
|
||||||
|
with:
|
||||||
|
args: ". --check -l 80 -S"
|
10
Dockerfile
10
Dockerfile
|
@ -2,14 +2,12 @@ FROM python:3.9
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app /app/static /app/images
|
||||||
RUN mkdir /app/static
|
|
||||||
RUN mkdir /app/images
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt /app/
|
COPY requirements.txt /app/
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt --no-cache-dir
|
||||||
|
RUN apt-get update && apt-get install -y gettext libgettextpo-dev && apt-get clean
|
||||||
|
|
||||||
COPY ./bookwyrm /app
|
COPY ./bookwyrm ./celerywyrm /app/
|
||||||
COPY ./celerywyrm /app
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ Code contributions are gladly welcomed! If you're not sure where to start, take
|
||||||
If you have questions about the project or contributing, you can set up a video call during BookWyrm ["office hours"](https://calendly.com/mouse-reeve/30min).
|
If you have questions about the project or contributing, you can set up a video call during BookWyrm ["office hours"](https://calendly.com/mouse-reeve/30min).
|
||||||
|
|
||||||
### Translation
|
### Translation
|
||||||
Do you speak a language besides English? BookWyrm needs localization! If you're comfortable using git and want to get into the code, there are [instructions](#workin-with-translations-and-locale-files) on how to create and edit localization files. If you feel more comfortable working in a regular text editor and would prefer not to run the application, get in touch directly and we can figure out a system, like emailing a text file, that works best.
|
Do you speak a language besides English? BookWyrm needs localization! If you're comfortable using git and want to get into the code, there are [instructions](#working-with-translations-and-locale-files) on how to create and edit localization files. If you feel more comfortable working in a regular text editor and would prefer not to run the application, get in touch directly and we can figure out a system, like emailing a text file, that works best.
|
||||||
|
|
||||||
### Financial Support
|
### Financial Support
|
||||||
BookWyrm is an ad-free passion project with no intentions of seeking out venture funding or corporate financial relationships. If you want to help keep the project going, you can donate to the [Patreon](https://www.patreon.com/bookwyrm), or make a one time gift via [PayPal](https://paypal.me/oulipo).
|
BookWyrm is an ad-free passion project with no intentions of seeking out venture funding or corporate financial relationships. If you want to help keep the project going, you can donate to the [Patreon](https://www.patreon.com/bookwyrm), or make a one time gift via [PayPal](https://paypal.me/oulipo).
|
||||||
|
@ -118,7 +118,7 @@ If you edit the CSS or JavaScript, you will need to run Django's `collectstatic`
|
||||||
./bw-dev collectstatic
|
./bw-dev collectstatic
|
||||||
```
|
```
|
||||||
|
|
||||||
### Workin with translations and locale files
|
### Working with translations and locale files
|
||||||
Text in the html files are wrapped in translation tags (`{% trans %}` and `{% blocktrans %}`), and Django generates locale files for all the strings in which you can add translations for the text. You can find existing translations in the `locale/` directory.
|
Text in the html files are wrapped in translation tags (`{% trans %}` and `{% blocktrans %}`), and Django generates locale files for all the strings in which you can add translations for the text. You can find existing translations in the `locale/` directory.
|
||||||
|
|
||||||
The application's language is set by a request header sent by your browser to the application, so to change the language of the application, you can change the default language requested by your browser.
|
The application's language is set by a request header sent by your browser to the application, so to change the language of the application, you can change the default language requested by your browser.
|
||||||
|
@ -132,7 +132,10 @@ To start translation into a language which is currently supported, run the djang
|
||||||
#### Editing a locale
|
#### Editing a locale
|
||||||
When you have a locale file, open the `django.po` in the directory for the language (for example, if you were adding German, `locale/de/LC_MESSAGES/django.po`. All the the text in the application will be shown in paired strings, with `msgid` as the original text, and `msgstr` as the translation (by default, this is set to an empty string, and will display the original text).
|
When you have a locale file, open the `django.po` in the directory for the language (for example, if you were adding German, `locale/de/LC_MESSAGES/django.po`. All the the text in the application will be shown in paired strings, with `msgid` as the original text, and `msgstr` as the translation (by default, this is set to an empty string, and will display the original text).
|
||||||
|
|
||||||
Add you translations to the `msgstr` strings, and when you're ready, compile the locale by running:
|
Add your translations to the `msgstr` strings. As the messages in the application are updated, `gettext` will sometimes add best-guess fuzzy matched options for those translations. When a message is marked as fuzzy, it will not be used in the application, so be sure to remove it when you translate that line.
|
||||||
|
|
||||||
|
When you're done, compile the locale by running:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
./bw-dev compilemessages
|
./bw-dev compilemessages
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' bring activitypub functions into the namespace '''
|
""" bring activitypub functions into the namespace """
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -22,9 +22,9 @@ from .verbs import Announce, Like
|
||||||
# this creates a list of all the Activity types that we can serialize,
|
# 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
|
# so when an Activity comes in from outside, we can check if it's known
|
||||||
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
||||||
activity_objects = {c[0]: c[1] for c in cls_members \
|
activity_objects = {c[0]: c[1] for c in cls_members if hasattr(c[1], "to_model")}
|
||||||
if hasattr(c[1], 'to_model')}
|
|
||||||
|
|
||||||
def parse(activity_json):
|
def parse(activity_json):
|
||||||
''' figure out what activity this is and parse it '''
|
""" figure out what activity this is and parse it """
|
||||||
return naive_parse(activity_objects, activity_json)
|
return naive_parse(activity_objects, activity_json)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' basics for an activitypub serializer '''
|
""" basics for an activitypub serializer """
|
||||||
from dataclasses import dataclass, fields, MISSING
|
from dataclasses import dataclass, fields, MISSING
|
||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
|
|
||||||
|
@ -8,46 +8,52 @@ from django.db import IntegrityError, transaction
|
||||||
from bookwyrm.connectors import ConnectorException, get_data
|
from bookwyrm.connectors import ConnectorException, get_data
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
|
||||||
class ActivitySerializerError(ValueError):
|
class ActivitySerializerError(ValueError):
|
||||||
''' routine problems serializing activitypub json '''
|
""" routine problems serializing activitypub json """
|
||||||
|
|
||||||
|
|
||||||
class ActivityEncoder(JSONEncoder):
|
class ActivityEncoder(JSONEncoder):
|
||||||
''' used to convert an Activity object into json '''
|
""" used to convert an Activity object into json """
|
||||||
|
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
return o.__dict__
|
return o.__dict__
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Link:
|
class Link:
|
||||||
''' for tagging a book in a status '''
|
""" for tagging a book in a status """
|
||||||
|
|
||||||
href: str
|
href: str
|
||||||
name: str
|
name: str
|
||||||
type: str = 'Link'
|
type: str = "Link"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Mention(Link):
|
class Mention(Link):
|
||||||
''' a subtype of Link for mentioning an actor '''
|
""" a subtype of Link for mentioning an actor """
|
||||||
type: str = 'Mention'
|
|
||||||
|
type: str = "Mention"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Signature:
|
class Signature:
|
||||||
''' public key block '''
|
""" public key block """
|
||||||
|
|
||||||
creator: str
|
creator: str
|
||||||
created: str
|
created: str
|
||||||
signatureValue: str
|
signatureValue: str
|
||||||
type: str = 'RsaSignature2017'
|
type: str = "RsaSignature2017"
|
||||||
|
|
||||||
|
|
||||||
def naive_parse(activity_objects, activity_json, serializer=None):
|
def naive_parse(activity_objects, activity_json, serializer=None):
|
||||||
''' this navigates circular import issues '''
|
""" this navigates circular import issues """
|
||||||
if not serializer:
|
if not serializer:
|
||||||
if activity_json.get('publicKeyPem'):
|
if activity_json.get("publicKeyPem"):
|
||||||
# ugh
|
# ugh
|
||||||
activity_json['type'] = 'PublicKey'
|
activity_json["type"] = "PublicKey"
|
||||||
try:
|
try:
|
||||||
activity_type = activity_json['type']
|
activity_type = activity_json["type"]
|
||||||
serializer = activity_objects[activity_type]
|
serializer = activity_objects[activity_type]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise ActivitySerializerError(e)
|
raise ActivitySerializerError(e)
|
||||||
|
@ -57,14 +63,15 @@ def naive_parse(activity_objects, activity_json, serializer=None):
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class ActivityObject:
|
class ActivityObject:
|
||||||
''' actor activitypub json '''
|
""" actor activitypub json """
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
def __init__(self, activity_objects=None, **kwargs):
|
def __init__(self, activity_objects=None, **kwargs):
|
||||||
''' this lets you pass in an object with fields that aren't in the
|
"""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
|
dataclass, which it ignores. Any field in the dataclass is required or
|
||||||
has a default value '''
|
has a default value"""
|
||||||
for field in fields(self):
|
for field in fields(self):
|
||||||
try:
|
try:
|
||||||
value = kwargs[field.name]
|
value = kwargs[field.name]
|
||||||
|
@ -75,7 +82,7 @@ class ActivityObject:
|
||||||
except TypeError:
|
except TypeError:
|
||||||
is_subclass = False
|
is_subclass = False
|
||||||
# serialize a model obj
|
# serialize a model obj
|
||||||
if hasattr(value, 'to_activity'):
|
if hasattr(value, "to_activity"):
|
||||||
value = value.to_activity()
|
value = value.to_activity()
|
||||||
# parse a dict into the appropriate activity
|
# parse a dict into the appropriate activity
|
||||||
elif is_subclass and isinstance(value, dict):
|
elif is_subclass and isinstance(value, dict):
|
||||||
|
@ -83,26 +90,28 @@ class ActivityObject:
|
||||||
value = naive_parse(activity_objects, value)
|
value = naive_parse(activity_objects, value)
|
||||||
else:
|
else:
|
||||||
value = naive_parse(
|
value = naive_parse(
|
||||||
activity_objects, value, serializer=field.type)
|
activity_objects, value, serializer=field.type
|
||||||
|
)
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if field.default == MISSING and \
|
if field.default == MISSING and field.default_factory == MISSING:
|
||||||
field.default_factory == MISSING:
|
raise ActivitySerializerError(
|
||||||
raise ActivitySerializerError(\
|
"Missing required field: %s" % field.name
|
||||||
'Missing required field: %s' % field.name)
|
)
|
||||||
value = field.default
|
value = field.default
|
||||||
setattr(self, field.name, value)
|
setattr(self, field.name, value)
|
||||||
|
|
||||||
|
|
||||||
def to_model(self, model=None, instance=None, allow_create=True, save=True):
|
def to_model(self, model=None, instance=None, allow_create=True, save=True):
|
||||||
''' convert from an activity to a model instance '''
|
""" convert from an activity to a model instance """
|
||||||
model = model or get_model_from_type(self.type)
|
model = model or get_model_from_type(self.type)
|
||||||
|
|
||||||
# only reject statuses if we're potentially creating them
|
# only reject statuses if we're potentially creating them
|
||||||
if allow_create and \
|
if (
|
||||||
hasattr(model, 'ignore_activity') and \
|
allow_create
|
||||||
model.ignore_activity(self):
|
and hasattr(model, "ignore_activity")
|
||||||
return None
|
and model.ignore_activity(self)
|
||||||
|
):
|
||||||
|
raise ActivitySerializerError()
|
||||||
|
|
||||||
# check for an existing instance
|
# check for an existing instance
|
||||||
instance = instance or model.find_existing(self.serialize())
|
instance = instance or model.find_existing(self.serialize())
|
||||||
|
@ -142,8 +151,10 @@ class ActivityObject:
|
||||||
field.set_field_from_activity(instance, self)
|
field.set_field_from_activity(instance, self)
|
||||||
|
|
||||||
# reversed relationships in the models
|
# reversed relationships in the models
|
||||||
for (model_field_name, activity_field_name) in \
|
for (
|
||||||
instance.deserialize_reverse_fields:
|
model_field_name,
|
||||||
|
activity_field_name,
|
||||||
|
) in instance.deserialize_reverse_fields:
|
||||||
# attachments on Status, for example
|
# attachments on Status, for example
|
||||||
values = getattr(self, activity_field_name)
|
values = getattr(self, activity_field_name)
|
||||||
if values is None or values is MISSING:
|
if values is None or values is MISSING:
|
||||||
|
@ -161,13 +172,12 @@ class ActivityObject:
|
||||||
instance.__class__.__name__,
|
instance.__class__.__name__,
|
||||||
related_field_name,
|
related_field_name,
|
||||||
instance.remote_id,
|
instance.remote_id,
|
||||||
item
|
item,
|
||||||
)
|
)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
''' convert to dictionary with context attr '''
|
""" convert to dictionary with context attr """
|
||||||
data = self.__dict__.copy()
|
data = self.__dict__.copy()
|
||||||
# recursively serialize
|
# recursively serialize
|
||||||
for (k, v) in data.items():
|
for (k, v) in data.items():
|
||||||
|
@ -177,21 +187,18 @@ class ActivityObject:
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
data = {k: v for (k, v) in data.items() if v is not None}
|
data = {k: v for (k, v) in data.items() if v is not None}
|
||||||
data['@context'] = 'https://www.w3.org/ns/activitystreams'
|
data["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def set_related_field(
|
def set_related_field(
|
||||||
model_name, origin_model_name, related_field_name,
|
model_name, origin_model_name, related_field_name, related_remote_id, data
|
||||||
related_remote_id, data):
|
):
|
||||||
''' load reverse related fields (editions, attachments) without blocking '''
|
""" load reverse related fields (editions, attachments) without blocking """
|
||||||
model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True)
|
model = apps.get_model("bookwyrm.%s" % model_name, require_ready=True)
|
||||||
origin_model = apps.get_model(
|
origin_model = apps.get_model("bookwyrm.%s" % origin_model_name, require_ready=True)
|
||||||
'bookwyrm.%s' % origin_model_name,
|
|
||||||
require_ready=True
|
|
||||||
)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
|
@ -205,42 +212,44 @@ def set_related_field(
|
||||||
# this must exist because it's the object that triggered this function
|
# this must exist because it's the object that triggered this function
|
||||||
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise ValueError(
|
raise ValueError("Invalid related remote id: %s" % related_remote_id)
|
||||||
'Invalid related remote id: %s' % related_remote_id)
|
|
||||||
|
|
||||||
# set the origin's remote id on the activity so it will be there when
|
# set the origin's remote id on the activity so it will be there when
|
||||||
# the model instance is created
|
# the model instance is created
|
||||||
# edition.parentWork = instance, for example
|
# edition.parentWork = instance, for example
|
||||||
model_field = getattr(model, related_field_name)
|
model_field = getattr(model, related_field_name)
|
||||||
if hasattr(model_field, 'activitypub_field'):
|
if hasattr(model_field, "activitypub_field"):
|
||||||
setattr(
|
setattr(
|
||||||
activity,
|
activity, getattr(model_field, "activitypub_field"), instance.remote_id
|
||||||
getattr(model_field, 'activitypub_field'),
|
|
||||||
instance.remote_id
|
|
||||||
)
|
)
|
||||||
item = activity.to_model()
|
item = activity.to_model()
|
||||||
|
|
||||||
# if the related field isn't serialized (attachments on Status), then
|
# if the related field isn't serialized (attachments on Status), then
|
||||||
# we have to set it post-creation
|
# we have to set it post-creation
|
||||||
if not hasattr(model_field, 'activitypub_field'):
|
if not hasattr(model_field, "activitypub_field"):
|
||||||
setattr(item, related_field_name, instance)
|
setattr(item, related_field_name, instance)
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
def get_model_from_type(activity_type):
|
def get_model_from_type(activity_type):
|
||||||
''' given the activity, what type of model '''
|
""" given the activity, what type of model """
|
||||||
models = apps.get_models()
|
models = apps.get_models()
|
||||||
model = [m for m in models if hasattr(m, 'activity_serializer') and \
|
model = [
|
||||||
hasattr(m.activity_serializer, 'type') and \
|
m
|
||||||
m.activity_serializer.type == activity_type]
|
for m in models
|
||||||
|
if hasattr(m, "activity_serializer")
|
||||||
|
and hasattr(m.activity_serializer, "type")
|
||||||
|
and m.activity_serializer.type == activity_type
|
||||||
|
]
|
||||||
if not model:
|
if not model:
|
||||||
raise ActivitySerializerError(
|
raise ActivitySerializerError(
|
||||||
'No model found for activity type "%s"' % activity_type)
|
'No model found for activity type "%s"' % activity_type
|
||||||
|
)
|
||||||
return model[0]
|
return model[0]
|
||||||
|
|
||||||
|
|
||||||
def resolve_remote_id(remote_id, model=None, refresh=False, save=True):
|
def resolve_remote_id(remote_id, model=None, refresh=False, save=True):
|
||||||
''' take a remote_id and return an instance, creating if necessary '''
|
""" take a remote_id and return an instance, creating if necessary """
|
||||||
if model: # a bonus check we can do if we already know the model
|
if model: # a bonus check we can do if we already know the model
|
||||||
result = model.find_existing_by_remote_id(remote_id)
|
result = model.find_existing_by_remote_id(remote_id)
|
||||||
if result and not refresh:
|
if result and not refresh:
|
||||||
|
@ -251,11 +260,12 @@ def resolve_remote_id(remote_id, model=None, refresh=False, save=True):
|
||||||
data = get_data(remote_id)
|
data = get_data(remote_id)
|
||||||
except (ConnectorException, ConnectionError):
|
except (ConnectorException, ConnectionError):
|
||||||
raise ActivitySerializerError(
|
raise ActivitySerializerError(
|
||||||
'Could not connect to host for remote_id in %s model: %s' % \
|
"Could not connect to host for remote_id in %s model: %s"
|
||||||
(model.__name__, remote_id))
|
% (model.__name__, remote_id)
|
||||||
|
)
|
||||||
# determine the model implicitly, if not provided
|
# determine the model implicitly, if not provided
|
||||||
if not model:
|
if not model:
|
||||||
model = get_model_from_type(data.get('type'))
|
model = get_model_from_type(data.get("type"))
|
||||||
|
|
||||||
# check for existing items with shared unique identifiers
|
# check for existing items with shared unique identifiers
|
||||||
result = model.find_existing(data)
|
result = model.find_existing(data)
|
||||||
|
|
|
@ -1,70 +1,75 @@
|
||||||
''' book and author data '''
|
""" book and author data """
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from .base_activity import ActivityObject
|
from .base_activity import ActivityObject
|
||||||
from .image import Image
|
from .image import Image
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Book(ActivityObject):
|
class Book(ActivityObject):
|
||||||
''' serializes an edition or work, abstract '''
|
""" serializes an edition or work, abstract """
|
||||||
|
|
||||||
title: str
|
title: str
|
||||||
sortTitle: str = ''
|
sortTitle: str = ""
|
||||||
subtitle: str = ''
|
subtitle: str = ""
|
||||||
description: str = ''
|
description: str = ""
|
||||||
languages: List[str] = field(default_factory=lambda: [])
|
languages: List[str] = field(default_factory=lambda: [])
|
||||||
series: str = ''
|
series: str = ""
|
||||||
seriesNumber: str = ''
|
seriesNumber: str = ""
|
||||||
subjects: List[str] = field(default_factory=lambda: [])
|
subjects: List[str] = field(default_factory=lambda: [])
|
||||||
subjectPlaces: List[str] = field(default_factory=lambda: [])
|
subjectPlaces: List[str] = field(default_factory=lambda: [])
|
||||||
|
|
||||||
authors: List[str] = field(default_factory=lambda: [])
|
authors: List[str] = field(default_factory=lambda: [])
|
||||||
firstPublishedDate: str = ''
|
firstPublishedDate: str = ""
|
||||||
publishedDate: str = ''
|
publishedDate: str = ""
|
||||||
|
|
||||||
openlibraryKey: str = ''
|
openlibraryKey: str = ""
|
||||||
librarythingKey: str = ''
|
librarythingKey: str = ""
|
||||||
goodreadsKey: str = ''
|
goodreadsKey: str = ""
|
||||||
|
|
||||||
cover: Image = field(default_factory=lambda: {})
|
cover: Image = None
|
||||||
type: str = 'Book'
|
type: str = "Book"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Edition(Book):
|
class Edition(Book):
|
||||||
''' Edition instance of a book object '''
|
""" Edition instance of a book object """
|
||||||
|
|
||||||
work: str
|
work: str
|
||||||
isbn10: str = ''
|
isbn10: str = ""
|
||||||
isbn13: str = ''
|
isbn13: str = ""
|
||||||
oclcNumber: str = ''
|
oclcNumber: str = ""
|
||||||
asin: str = ''
|
asin: str = ""
|
||||||
pages: int = None
|
pages: int = None
|
||||||
physicalFormat: str = ''
|
physicalFormat: str = ""
|
||||||
publishers: List[str] = field(default_factory=lambda: [])
|
publishers: List[str] = field(default_factory=lambda: [])
|
||||||
editionRank: int = 0
|
editionRank: int = 0
|
||||||
|
|
||||||
type: str = 'Edition'
|
type: str = "Edition"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Work(Book):
|
class Work(Book):
|
||||||
''' work instance of a book object '''
|
""" work instance of a book object """
|
||||||
lccn: str = ''
|
|
||||||
defaultEdition: str = ''
|
lccn: str = ""
|
||||||
|
defaultEdition: str = ""
|
||||||
editions: List[str] = field(default_factory=lambda: [])
|
editions: List[str] = field(default_factory=lambda: [])
|
||||||
type: str = 'Work'
|
type: str = "Work"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Author(ActivityObject):
|
class Author(ActivityObject):
|
||||||
''' author of a book '''
|
""" author of a book """
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
born: str = None
|
born: str = None
|
||||||
died: str = None
|
died: str = None
|
||||||
aliases: List[str] = field(default_factory=lambda: [])
|
aliases: List[str] = field(default_factory=lambda: [])
|
||||||
bio: str = ''
|
bio: str = ""
|
||||||
openlibraryKey: str = ''
|
openlibraryKey: str = ""
|
||||||
librarythingKey: str = ''
|
librarythingKey: str = ""
|
||||||
goodreadsKey: str = ''
|
goodreadsKey: str = ""
|
||||||
wikipediaLink: str = ''
|
wikipediaLink: str = ""
|
||||||
type: str = 'Author'
|
type: str = "Author"
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
''' an image, nothing fancy '''
|
""" an image, nothing fancy """
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from .base_activity import ActivityObject
|
from .base_activity import ActivityObject
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Image(ActivityObject):
|
class Image(ActivityObject):
|
||||||
''' image block '''
|
""" image block """
|
||||||
|
|
||||||
url: str
|
url: str
|
||||||
name: str = ''
|
name: str = ""
|
||||||
type: str = 'Image'
|
type: str = "Image"
|
||||||
id: str = ''
|
id: str = ""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' note serializer and children thereof '''
|
""" note serializer and children thereof """
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -6,10 +6,12 @@ from django.apps import apps
|
||||||
from .base_activity import ActivityObject, Link
|
from .base_activity import ActivityObject, Link
|
||||||
from .image import Image
|
from .image import Image
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Tombstone(ActivityObject):
|
class Tombstone(ActivityObject):
|
||||||
''' the placeholder for a deleted status '''
|
""" the placeholder for a deleted status """
|
||||||
type: str = 'Tombstone'
|
|
||||||
|
type: str = "Tombstone"
|
||||||
|
|
||||||
def to_model(self, *args, **kwargs):# pylint: disable=unused-argument
|
def to_model(self, *args, **kwargs):# pylint: disable=unused-argument
|
||||||
''' this should never really get serialized, just searched for '''
|
''' this should never really get serialized, just searched for '''
|
||||||
|
@ -19,59 +21,66 @@ class Tombstone(ActivityObject):
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Note(ActivityObject):
|
class Note(ActivityObject):
|
||||||
''' Note activity '''
|
""" Note activity """
|
||||||
|
|
||||||
published: str
|
published: str
|
||||||
attributedTo: str
|
attributedTo: str
|
||||||
content: str = ''
|
content: str = ""
|
||||||
to: List[str] = field(default_factory=lambda: [])
|
to: List[str] = field(default_factory=lambda: [])
|
||||||
cc: List[str] = field(default_factory=lambda: [])
|
cc: List[str] = field(default_factory=lambda: [])
|
||||||
replies: Dict = field(default_factory=lambda: {})
|
replies: Dict = field(default_factory=lambda: {})
|
||||||
inReplyTo: str = ''
|
inReplyTo: str = ""
|
||||||
summary: str = ''
|
summary: str = ""
|
||||||
tag: List[Link] = field(default_factory=lambda: [])
|
tag: List[Link] = field(default_factory=lambda: [])
|
||||||
attachment: List[Image] = field(default_factory=lambda: [])
|
attachment: List[Image] = field(default_factory=lambda: [])
|
||||||
sensitive: bool = False
|
sensitive: bool = False
|
||||||
type: str = 'Note'
|
type: str = "Note"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Article(Note):
|
class Article(Note):
|
||||||
''' what's an article except a note with more fields '''
|
""" what's an article except a note with more fields """
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
type: str = 'Article'
|
type: str = "Article"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class GeneratedNote(Note):
|
class GeneratedNote(Note):
|
||||||
''' just a re-typed note '''
|
""" just a re-typed note """
|
||||||
type: str = 'GeneratedNote'
|
|
||||||
|
type: str = "GeneratedNote"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Comment(Note):
|
class Comment(Note):
|
||||||
''' like a note but with a book '''
|
""" like a note but with a book """
|
||||||
|
|
||||||
inReplyToBook: str
|
inReplyToBook: str
|
||||||
type: str = 'Comment'
|
type: str = "Comment"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Quotation(Comment):
|
class Quotation(Comment):
|
||||||
''' a quote and commentary on a book '''
|
""" a quote and commentary on a book """
|
||||||
|
|
||||||
quote: str
|
quote: str
|
||||||
type: str = 'Quotation'
|
type: str = "Quotation"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Review(Comment):
|
class Review(Comment):
|
||||||
''' a full book review '''
|
""" a full book review """
|
||||||
|
|
||||||
name: str = None
|
name: str = None
|
||||||
rating: int = None
|
rating: int = None
|
||||||
type: str = 'Review'
|
type: str = "Review"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Rating(Comment):
|
class Rating(Comment):
|
||||||
''' just a star rating '''
|
""" just a star rating """
|
||||||
|
|
||||||
rating: int
|
rating: int
|
||||||
content: str = None
|
content: str = None
|
||||||
type: str = 'Rating'
|
type: str = "Rating"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' defines activitypub collections (lists) '''
|
""" defines activitypub collections (lists) """
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
@ -7,38 +7,46 @@ from .base_activity import ActivityObject
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class OrderedCollection(ActivityObject):
|
class OrderedCollection(ActivityObject):
|
||||||
''' structure of an ordered collection activity '''
|
""" structure of an ordered collection activity """
|
||||||
|
|
||||||
totalItems: int
|
totalItems: int
|
||||||
first: str
|
first: str
|
||||||
last: str = None
|
last: str = None
|
||||||
name: str = None
|
name: str = None
|
||||||
owner: str = None
|
owner: str = None
|
||||||
type: str = 'OrderedCollection'
|
type: str = "OrderedCollection"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class OrderedCollectionPrivate(OrderedCollection):
|
class OrderedCollectionPrivate(OrderedCollection):
|
||||||
''' an ordered collection with privacy settings '''
|
""" an ordered collection with privacy settings """
|
||||||
|
|
||||||
to: List[str] = field(default_factory=lambda: [])
|
to: List[str] = field(default_factory=lambda: [])
|
||||||
cc: List[str] = field(default_factory=lambda: [])
|
cc: List[str] = field(default_factory=lambda: [])
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Shelf(OrderedCollectionPrivate):
|
class Shelf(OrderedCollectionPrivate):
|
||||||
''' structure of an ordered collection activity '''
|
""" structure of an ordered collection activity """
|
||||||
type: str = 'Shelf'
|
|
||||||
|
type: str = "Shelf"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class BookList(OrderedCollectionPrivate):
|
class BookList(OrderedCollectionPrivate):
|
||||||
''' structure of an ordered collection activity '''
|
""" structure of an ordered collection activity """
|
||||||
|
|
||||||
summary: str = None
|
summary: str = None
|
||||||
curation: str = 'closed'
|
curation: str = "closed"
|
||||||
type: str = 'BookList'
|
type: str = "BookList"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class OrderedCollectionPage(ActivityObject):
|
class OrderedCollectionPage(ActivityObject):
|
||||||
''' structure of an ordered collection activity '''
|
""" structure of an ordered collection activity """
|
||||||
|
|
||||||
partOf: str
|
partOf: str
|
||||||
orderedItems: List
|
orderedItems: List
|
||||||
next: str = None
|
next: str = None
|
||||||
prev: str = None
|
prev: str = None
|
||||||
type: str = 'OrderedCollectionPage'
|
type: str = "OrderedCollectionPage"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' actor serializer '''
|
""" actor serializer """
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
@ -8,15 +8,17 @@ from .image import Image
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class PublicKey(ActivityObject):
|
class PublicKey(ActivityObject):
|
||||||
''' public key block '''
|
""" public key block """
|
||||||
|
|
||||||
owner: str
|
owner: str
|
||||||
publicKeyPem: str
|
publicKeyPem: str
|
||||||
type: str = 'PublicKey'
|
type: str = "PublicKey"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Person(ActivityObject):
|
class Person(ActivityObject):
|
||||||
''' actor activitypub json '''
|
""" actor activitypub json """
|
||||||
|
|
||||||
preferredUsername: str
|
preferredUsername: str
|
||||||
inbox: str
|
inbox: str
|
||||||
outbox: str
|
outbox: str
|
||||||
|
@ -29,4 +31,4 @@ class Person(ActivityObject):
|
||||||
bookwyrmUser: bool = False
|
bookwyrmUser: bool = False
|
||||||
manuallyApprovesFollowers: str = False
|
manuallyApprovesFollowers: str = False
|
||||||
discoverable: str = True
|
discoverable: str = True
|
||||||
type: str = 'Person'
|
type: str = "Person"
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django.http import JsonResponse
|
||||||
|
|
||||||
from .base_activity import ActivityEncoder
|
from .base_activity import ActivityEncoder
|
||||||
|
|
||||||
|
|
||||||
class ActivitypubResponse(JsonResponse):
|
class ActivitypubResponse(JsonResponse):
|
||||||
"""
|
"""
|
||||||
A class to be used in any place that's serializing responses for
|
A class to be used in any place that's serializing responses for
|
||||||
|
@ -9,10 +10,17 @@ class ActivitypubResponse(JsonResponse):
|
||||||
configures some stuff beforehand. Made to be a drop-in replacement of
|
configures some stuff beforehand. Made to be a drop-in replacement of
|
||||||
JsonResponse.
|
JsonResponse.
|
||||||
"""
|
"""
|
||||||
def __init__(self, data, encoder=ActivityEncoder, safe=False,
|
|
||||||
json_dumps_params=None, **kwargs):
|
|
||||||
|
|
||||||
if 'content_type' not in kwargs:
|
def __init__(
|
||||||
kwargs['content_type'] = 'application/activity+json'
|
self,
|
||||||
|
data,
|
||||||
|
encoder=ActivityEncoder,
|
||||||
|
safe=False,
|
||||||
|
json_dumps_params=None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
|
||||||
|
if "content_type" not in kwargs:
|
||||||
|
kwargs["content_type"] = "application/activity+json"
|
||||||
|
|
||||||
super().__init__(data, encoder, safe, json_dumps_params, **kwargs)
|
super().__init__(data, encoder, safe, json_dumps_params, **kwargs)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' undo wrapper activity '''
|
""" undo wrapper activity """
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
from typing import List
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -9,160 +9,173 @@ from .book import Edition
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Verb(ActivityObject):
|
class Verb(ActivityObject):
|
||||||
''' generic fields for activities - maybe an unecessary level of
|
"""generic fields for activities - maybe an unecessary level of
|
||||||
abstraction but w/e '''
|
abstraction but w/e"""
|
||||||
|
|
||||||
actor: str
|
actor: str
|
||||||
object: ActivityObject
|
object: ActivityObject
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' usually we just want to save, this can be overridden as needed '''
|
""" usually we just want to save, this can be overridden as needed """
|
||||||
self.object.to_model()
|
self.object.to_model()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Create(Verb):
|
class Create(Verb):
|
||||||
''' Create activity '''
|
""" Create activity """
|
||||||
|
|
||||||
to: List
|
to: List
|
||||||
cc: List
|
cc: List
|
||||||
signature: Signature = None
|
signature: Signature = None
|
||||||
type: str = 'Create'
|
type: str = "Create"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Delete(Verb):
|
class Delete(Verb):
|
||||||
''' Create activity '''
|
""" Create activity """
|
||||||
|
|
||||||
to: List
|
to: List
|
||||||
cc: List
|
cc: List
|
||||||
type: str = 'Delete'
|
type: str = "Delete"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' find and delete the activity object '''
|
""" find and delete the activity object """
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
obj = self.object.to_model(save=False, allow_create=False)
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Update(Verb):
|
class Update(Verb):
|
||||||
''' Update activity '''
|
""" Update activity """
|
||||||
|
|
||||||
to: List
|
to: List
|
||||||
type: str = 'Update'
|
type: str = "Update"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' update a model instance from the dataclass '''
|
""" update a model instance from the dataclass """
|
||||||
self.object.to_model(allow_create=False)
|
self.object.to_model(allow_create=False)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Undo(Verb):
|
class Undo(Verb):
|
||||||
''' Undo an activity '''
|
""" Undo an activity """
|
||||||
type: str = 'Undo'
|
|
||||||
|
type: str = "Undo"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' find and remove the activity object '''
|
""" find and remove the activity object """
|
||||||
# this is so hacky but it does make it work....
|
# this is so hacky but it does make it work....
|
||||||
# (because you Reject a request and Undo a follow
|
# (because you Reject a request and Undo a follow
|
||||||
model = None
|
model = None
|
||||||
if self.object.type == 'Follow':
|
if self.object.type == "Follow":
|
||||||
model = apps.get_model('bookwyrm.UserFollows')
|
model = apps.get_model("bookwyrm.UserFollows")
|
||||||
obj = self.object.to_model(model=model, save=False, allow_create=False)
|
obj = self.object.to_model(model=model, save=False, allow_create=False)
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Follow(Verb):
|
class Follow(Verb):
|
||||||
''' Follow activity '''
|
""" Follow activity """
|
||||||
|
|
||||||
object: str
|
object: str
|
||||||
type: str = 'Follow'
|
type: str = "Follow"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' relationship save '''
|
""" relationship save """
|
||||||
self.to_model()
|
self.to_model()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Block(Verb):
|
class Block(Verb):
|
||||||
''' Block activity '''
|
""" Block activity """
|
||||||
|
|
||||||
object: str
|
object: str
|
||||||
type: str = 'Block'
|
type: str = "Block"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' relationship save '''
|
""" relationship save """
|
||||||
self.to_model()
|
self.to_model()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Accept(Verb):
|
class Accept(Verb):
|
||||||
''' Accept activity '''
|
""" Accept activity """
|
||||||
|
|
||||||
object: Follow
|
object: Follow
|
||||||
type: str = 'Accept'
|
type: str = "Accept"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' find and remove the activity object '''
|
""" find and remove the activity object """
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
obj = self.object.to_model(save=False, allow_create=False)
|
||||||
obj.accept()
|
obj.accept()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Reject(Verb):
|
class Reject(Verb):
|
||||||
''' Reject activity '''
|
""" Reject activity """
|
||||||
|
|
||||||
object: Follow
|
object: Follow
|
||||||
type: str = 'Reject'
|
type: str = "Reject"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' find and remove the activity object '''
|
""" find and remove the activity object """
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
obj = self.object.to_model(save=False, allow_create=False)
|
||||||
obj.reject()
|
obj.reject()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Add(Verb):
|
class Add(Verb):
|
||||||
'''Add activity '''
|
"""Add activity """
|
||||||
|
|
||||||
target: str
|
target: str
|
||||||
object: Edition
|
object: Edition
|
||||||
type: str = 'Add'
|
type: str = "Add"
|
||||||
notes: str = None
|
notes: str = None
|
||||||
order: int = 0
|
order: int = 0
|
||||||
approved: bool = True
|
approved: bool = True
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' add obj to collection '''
|
""" add obj to collection """
|
||||||
target = resolve_remote_id(self.target, refresh=False)
|
target = resolve_remote_id(self.target, refresh=False)
|
||||||
# we want to related field that isn't the book, this is janky af sorry
|
# we want to related field that isn't the book, this is janky af sorry
|
||||||
model = [t for t in type(target)._meta.related_objects \
|
model = [t for t in type(target)._meta.related_objects if t.name != "edition"][
|
||||||
if t.name != 'edition'][0].related_model
|
0
|
||||||
|
].related_model
|
||||||
self.to_model(model=model)
|
self.to_model(model=model)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Remove(Verb):
|
class Remove(Verb):
|
||||||
'''Remove activity '''
|
"""Remove activity """
|
||||||
|
|
||||||
target: ActivityObject
|
target: ActivityObject
|
||||||
type: str = 'Remove'
|
type: str = "Remove"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' find and remove the activity object '''
|
""" find and remove the activity object """
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
obj = self.object.to_model(save=False, allow_create=False)
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Like(Verb):
|
class Like(Verb):
|
||||||
''' a user faving an object '''
|
""" a user faving an object """
|
||||||
|
|
||||||
object: str
|
object: str
|
||||||
type: str = 'Like'
|
type: str = "Like"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' like '''
|
""" like """
|
||||||
self.to_model()
|
self.to_model()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Announce(Verb):
|
class Announce(Verb):
|
||||||
''' boosting a status '''
|
""" boosting a status """
|
||||||
|
|
||||||
object: str
|
object: str
|
||||||
type: str = 'Announce'
|
type: str = "Announce"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
''' boost '''
|
""" boost """
|
||||||
self.to_model()
|
self.to_model()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' models that will show up in django admin for superuser '''
|
""" models that will show up in django admin for superuser """
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' bring connectors into the namespace '''
|
""" bring connectors into the namespace """
|
||||||
from .settings import CONNECTORS
|
from .settings import CONNECTORS
|
||||||
from .abstract_connector import ConnectorException
|
from .abstract_connector import ConnectorException
|
||||||
from .abstract_connector import get_data, get_image
|
from .abstract_connector import get_data, get_image
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' functionality outline for a book data connector '''
|
""" functionality outline for a book data connector """
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
import logging
|
import logging
|
||||||
|
@ -13,8 +13,11 @@ from .connector_manager import load_more_data, ConnectorException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AbstractMinimalConnector(ABC):
|
class AbstractMinimalConnector(ABC):
|
||||||
''' just the bare bones, for other bookwyrm instances '''
|
""" just the bare bones, for other bookwyrm instances """
|
||||||
|
|
||||||
def __init__(self, identifier):
|
def __init__(self, identifier):
|
||||||
# load connector settings
|
# load connector settings
|
||||||
info = models.Connector.objects.get(identifier=identifier)
|
info = models.Connector.objects.get(identifier=identifier)
|
||||||
|
@ -22,30 +25,31 @@ class AbstractMinimalConnector(ABC):
|
||||||
|
|
||||||
# the things in the connector model to copy over
|
# the things in the connector model to copy over
|
||||||
self_fields = [
|
self_fields = [
|
||||||
'base_url',
|
"base_url",
|
||||||
'books_url',
|
"books_url",
|
||||||
'covers_url',
|
"covers_url",
|
||||||
'search_url',
|
"search_url",
|
||||||
'max_query_count',
|
"isbn_search_url",
|
||||||
'name',
|
"max_query_count",
|
||||||
'identifier',
|
"name",
|
||||||
'local'
|
"identifier",
|
||||||
|
"local",
|
||||||
]
|
]
|
||||||
for field in self_fields:
|
for field in self_fields:
|
||||||
setattr(self, field, getattr(info, field))
|
setattr(self, field, getattr(info, field))
|
||||||
|
|
||||||
def search(self, query, min_confidence=None):
|
def search(self, query, min_confidence=None):
|
||||||
''' free text search '''
|
""" free text search """
|
||||||
params = {}
|
params = {}
|
||||||
if min_confidence:
|
if min_confidence:
|
||||||
params['min_confidence'] = min_confidence
|
params["min_confidence"] = min_confidence
|
||||||
|
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
'%s%s' % (self.search_url, query),
|
"%s%s" % (self.search_url, query),
|
||||||
params=params,
|
params=params,
|
||||||
headers={
|
headers={
|
||||||
'Accept': 'application/json; charset=utf-8',
|
"Accept": "application/json; charset=utf-8",
|
||||||
'User-Agent': settings.USER_AGENT,
|
"User-Agent": settings.USER_AGENT,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
|
@ -54,50 +58,82 @@ class AbstractMinimalConnector(ABC):
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
raise ConnectorException('Unable to parse json response', e)
|
raise ConnectorException("Unable to parse json response", e)
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for doc in self.parse_search_data(data)[:10]:
|
for doc in self.parse_search_data(data)[:10]:
|
||||||
results.append(self.format_search_result(doc))
|
results.append(self.format_search_result(doc))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def isbn_search(self, query):
|
||||||
|
""" isbn search """
|
||||||
|
params = {}
|
||||||
|
resp = requests.get(
|
||||||
|
"%s%s" % (self.isbn_search_url, query),
|
||||||
|
params=params,
|
||||||
|
headers={
|
||||||
|
"Accept": "application/json; charset=utf-8",
|
||||||
|
"User-Agent": settings.USER_AGENT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not resp.ok:
|
||||||
|
resp.raise_for_status()
|
||||||
|
try:
|
||||||
|
data = resp.json()
|
||||||
|
except ValueError as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise ConnectorException("Unable to parse json response", e)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for doc in self.parse_isbn_search_data(data):
|
||||||
|
results.append(self.format_isbn_search_result(doc))
|
||||||
|
return results
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
''' pull up a book record by whatever means possible '''
|
""" pull up a book record by whatever means possible """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
''' turn the result json from a search into a list '''
|
""" turn the result json from a search into a list """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
''' create a SearchResult obj from json '''
|
""" create a SearchResult obj from json """
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
""" turn the result json from a search into a list """
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
""" create a SearchResult obj from json """
|
||||||
|
|
||||||
|
|
||||||
class AbstractConnector(AbstractMinimalConnector):
|
class AbstractConnector(AbstractMinimalConnector):
|
||||||
''' generic book data connector '''
|
""" generic book data connector """
|
||||||
|
|
||||||
def __init__(self, identifier):
|
def __init__(self, identifier):
|
||||||
super().__init__(identifier)
|
super().__init__(identifier)
|
||||||
# fields we want to look for in book data to copy over
|
# fields we want to look for in book data to copy over
|
||||||
# title we handle separately.
|
# title we handle separately.
|
||||||
self.book_mappings = []
|
self.book_mappings = []
|
||||||
|
|
||||||
|
|
||||||
def is_available(self):
|
def is_available(self):
|
||||||
''' check if you're allowed to use this connector '''
|
""" check if you're allowed to use this connector """
|
||||||
if self.max_query_count is not None:
|
if self.max_query_count is not None:
|
||||||
if self.connector.query_count >= self.max_query_count:
|
if self.connector.query_count >= self.max_query_count:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
''' translate arbitrary json into an Activitypub dataclass '''
|
""" translate arbitrary json into an Activitypub dataclass """
|
||||||
# first, check if we have the origin_id saved
|
# first, check if we have the origin_id saved
|
||||||
existing = models.Edition.find_existing_by_remote_id(remote_id) or \
|
existing = models.Edition.find_existing_by_remote_id(
|
||||||
models.Work.find_existing_by_remote_id(remote_id)
|
remote_id
|
||||||
|
) or models.Work.find_existing_by_remote_id(remote_id)
|
||||||
if existing:
|
if existing:
|
||||||
if hasattr(existing, 'get_default_editon'):
|
if hasattr(existing, "get_default_editon"):
|
||||||
return existing.get_default_editon()
|
return existing.get_default_editon()
|
||||||
return existing
|
return existing
|
||||||
|
|
||||||
|
@ -121,7 +157,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
edition_data = data
|
edition_data = data
|
||||||
|
|
||||||
if not work_data or not edition_data:
|
if not work_data or not edition_data:
|
||||||
raise ConnectorException('Unable to load book data: %s' % remote_id)
|
raise ConnectorException("Unable to load book data: %s" % remote_id)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# create activitypub object
|
# create activitypub object
|
||||||
|
@ -135,11 +171,10 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
load_more_data.delay(self.connector.id, work.id)
|
load_more_data.delay(self.connector.id, work.id)
|
||||||
return edition
|
return edition
|
||||||
|
|
||||||
|
|
||||||
def create_edition_from_data(self, work, edition_data):
|
def create_edition_from_data(self, work, edition_data):
|
||||||
''' if we already have the work, we're ready '''
|
""" if we already have the work, we're ready """
|
||||||
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
||||||
mapped_data['work'] = work.remote_id
|
mapped_data["work"] = work.remote_id
|
||||||
edition_activity = activitypub.Edition(**mapped_data)
|
edition_activity = activitypub.Edition(**mapped_data)
|
||||||
edition = edition_activity.to_model(model=models.Edition)
|
edition = edition_activity.to_model(model=models.Edition)
|
||||||
edition.connector = self.connector
|
edition.connector = self.connector
|
||||||
|
@ -156,9 +191,8 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
|
|
||||||
return edition
|
return edition
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_author(self, remote_id):
|
def get_or_create_author(self, remote_id):
|
||||||
''' load that author '''
|
""" load that author """
|
||||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||||
if existing:
|
if existing:
|
||||||
return existing
|
return existing
|
||||||
|
@ -170,31 +204,30 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
# this will dedupe
|
# this will dedupe
|
||||||
return activity.to_model(model=models.Author)
|
return activity.to_model(model=models.Author)
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
''' differentiate works and editions '''
|
""" differentiate works and editions """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_edition_from_work_data(self, data):
|
def get_edition_from_work_data(self, data):
|
||||||
''' every work needs at least one edition '''
|
""" every work needs at least one edition """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_work_from_edition_data(self, data):
|
def get_work_from_edition_data(self, data):
|
||||||
''' every edition needs a work '''
|
""" every edition needs a work """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_authors_from_data(self, data):
|
def get_authors_from_data(self, data):
|
||||||
''' load author data '''
|
""" load author data """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def expand_book_data(self, book):
|
def expand_book_data(self, book):
|
||||||
''' get more info on a book '''
|
""" get more info on a book """
|
||||||
|
|
||||||
|
|
||||||
def dict_from_mappings(data, mappings):
|
def dict_from_mappings(data, mappings):
|
||||||
''' create a dict in Activitypub format, using mappings supplies by
|
"""create a dict in Activitypub format, using mappings supplies by
|
||||||
the subclass '''
|
the subclass"""
|
||||||
result = {}
|
result = {}
|
||||||
for mapping in mappings:
|
for mapping in mappings:
|
||||||
result[mapping.local_field] = mapping.get_value(data)
|
result[mapping.local_field] = mapping.get_value(data)
|
||||||
|
@ -202,13 +235,13 @@ def dict_from_mappings(data, mappings):
|
||||||
|
|
||||||
|
|
||||||
def get_data(url):
|
def get_data(url):
|
||||||
''' wrapper for request.get '''
|
""" wrapper for request.get """
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
url,
|
url,
|
||||||
headers={
|
headers={
|
||||||
'Accept': 'application/json; charset=utf-8',
|
"Accept": "application/json; charset=utf-8",
|
||||||
'User-Agent': settings.USER_AGENT,
|
"User-Agent": settings.USER_AGENT,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except (RequestError, SSLError) as e:
|
except (RequestError, SSLError) as e:
|
||||||
|
@ -227,12 +260,12 @@ def get_data(url):
|
||||||
|
|
||||||
|
|
||||||
def get_image(url):
|
def get_image(url):
|
||||||
''' wrapper for requesting an image '''
|
""" wrapper for requesting an image """
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
url,
|
url,
|
||||||
headers={
|
headers={
|
||||||
'User-Agent': settings.USER_AGENT,
|
"User-Agent": settings.USER_AGENT,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except (RequestError, SSLError) as e:
|
except (RequestError, SSLError) as e:
|
||||||
|
@ -245,7 +278,8 @@ def get_image(url):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SearchResult:
|
class SearchResult:
|
||||||
''' standardized search result object '''
|
""" standardized search result object """
|
||||||
|
|
||||||
title: str
|
title: str
|
||||||
key: str
|
key: str
|
||||||
author: str
|
author: str
|
||||||
|
@ -255,17 +289,19 @@ class SearchResult:
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||||
self.key, self.title, self.author)
|
self.key, self.title, self.author
|
||||||
|
)
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
''' serialize a connector for json response '''
|
""" serialize a connector for json response """
|
||||||
serialized = asdict(self)
|
serialized = asdict(self)
|
||||||
del serialized['connector']
|
del serialized["connector"]
|
||||||
return serialized
|
return serialized
|
||||||
|
|
||||||
|
|
||||||
class Mapping:
|
class Mapping:
|
||||||
''' associate a local database field with a field in an external dataset '''
|
""" associate a local database field with a field in an external dataset """
|
||||||
|
|
||||||
def __init__(self, local_field, remote_field=None, formatter=None):
|
def __init__(self, local_field, remote_field=None, formatter=None):
|
||||||
noop = lambda x: x
|
noop = lambda x: x
|
||||||
|
|
||||||
|
@ -274,7 +310,7 @@ class Mapping:
|
||||||
self.formatter = formatter or noop
|
self.formatter = formatter or noop
|
||||||
|
|
||||||
def get_value(self, data):
|
def get_value(self, data):
|
||||||
''' pull a field from incoming json and return the formatted version '''
|
""" pull a field from incoming json and return the formatted version """
|
||||||
value = data.get(self.remote_field)
|
value = data.get(self.remote_field)
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
''' using another bookwyrm instance as a source of book data '''
|
""" using another bookwyrm instance as a source of book data """
|
||||||
from bookwyrm import activitypub, models
|
from bookwyrm import activitypub, models
|
||||||
from .abstract_connector import AbstractMinimalConnector, SearchResult
|
from .abstract_connector import AbstractMinimalConnector, SearchResult
|
||||||
|
|
||||||
|
|
||||||
class Connector(AbstractMinimalConnector):
|
class Connector(AbstractMinimalConnector):
|
||||||
''' this is basically just for search '''
|
""" this is basically just for search """
|
||||||
|
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
edition = activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
edition = activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
||||||
|
@ -17,5 +17,12 @@ class Connector(AbstractMinimalConnector):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
search_result['connector'] = self
|
search_result["connector"] = self
|
||||||
|
return SearchResult(**search_result)
|
||||||
|
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
search_result["connector"] = self
|
||||||
return SearchResult(**search_result)
|
return SearchResult(**search_result)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
''' interface with whatever connectors the app has '''
|
""" interface with whatever connectors the app has """
|
||||||
import importlib
|
import importlib
|
||||||
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
@ -9,40 +10,65 @@ from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
|
||||||
class ConnectorException(HTTPError):
|
class ConnectorException(HTTPError):
|
||||||
''' when the connector can't do what was asked '''
|
""" when the connector can't do what was asked """
|
||||||
|
|
||||||
|
|
||||||
def search(query, min_confidence=0.1):
|
def search(query, min_confidence=0.1):
|
||||||
''' find books based on arbitary keywords '''
|
""" find books based on arbitary keywords """
|
||||||
results = []
|
results = []
|
||||||
dedup_slug = lambda r: '%s/%s/%s' % (r.title, r.author, r.year)
|
|
||||||
|
# Have we got a ISBN ?
|
||||||
|
isbn = re.sub("[\W_]", "", query)
|
||||||
|
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
|
||||||
|
|
||||||
|
dedup_slug = lambda r: "%s/%s/%s" % (r.title, r.author, r.year)
|
||||||
result_index = set()
|
result_index = set()
|
||||||
for connector in get_connectors():
|
for connector in get_connectors():
|
||||||
|
result_set = None
|
||||||
|
if maybe_isbn:
|
||||||
|
# Search on ISBN
|
||||||
|
if not connector.isbn_search_url or connector.isbn_search_url == "":
|
||||||
|
result_set = []
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
result_set = connector.isbn_search(isbn)
|
||||||
|
except (HTTPError, ConnectorException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# if no isbn search or results, we fallback to generic search
|
||||||
|
if result_set == None or result_set == []:
|
||||||
try:
|
try:
|
||||||
result_set = connector.search(query, min_confidence=min_confidence)
|
result_set = connector.search(query, min_confidence=min_confidence)
|
||||||
except (HTTPError, ConnectorException):
|
except (HTTPError, ConnectorException):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result_set = [r for r in result_set \
|
result_set = [r for r in result_set if dedup_slug(r) not in result_index]
|
||||||
if dedup_slug(r) not in result_index]
|
|
||||||
# `|=` concats two sets. WE ARE GETTING FANCY HERE
|
# `|=` concats two sets. WE ARE GETTING FANCY HERE
|
||||||
result_index |= set(dedup_slug(r) for r in result_set)
|
result_index |= set(dedup_slug(r) for r in result_set)
|
||||||
results.append({
|
results.append(
|
||||||
'connector': connector,
|
{
|
||||||
'results': result_set,
|
"connector": connector,
|
||||||
})
|
"results": result_set,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def local_search(query, min_confidence=0.1, raw=False):
|
def local_search(query, min_confidence=0.1, raw=False):
|
||||||
''' only look at local search results '''
|
""" only look at local search results """
|
||||||
connector = load_connector(models.Connector.objects.get(local=True))
|
connector = load_connector(models.Connector.objects.get(local=True))
|
||||||
return connector.search(query, min_confidence=min_confidence, raw=raw)
|
return connector.search(query, min_confidence=min_confidence, raw=raw)
|
||||||
|
|
||||||
|
|
||||||
|
def isbn_local_search(query, raw=False):
|
||||||
|
""" only look at local search results """
|
||||||
|
connector = load_connector(models.Connector.objects.get(local=True))
|
||||||
|
return connector.isbn_search(query, raw=raw)
|
||||||
|
|
||||||
|
|
||||||
def first_search_result(query, min_confidence=0.1):
|
def first_search_result(query, min_confidence=0.1):
|
||||||
''' search until you find a result that fits '''
|
""" search until you find a result that fits """
|
||||||
for connector in get_connectors():
|
for connector in get_connectors():
|
||||||
result = connector.search(query, min_confidence=min_confidence)
|
result = connector.search(query, min_confidence=min_confidence)
|
||||||
if result:
|
if result:
|
||||||
|
@ -51,29 +77,29 @@ def first_search_result(query, min_confidence=0.1):
|
||||||
|
|
||||||
|
|
||||||
def get_connectors():
|
def get_connectors():
|
||||||
''' load all connectors '''
|
""" load all connectors """
|
||||||
for info in models.Connector.objects.order_by('priority').all():
|
for info in models.Connector.objects.order_by("priority").all():
|
||||||
yield load_connector(info)
|
yield load_connector(info)
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_connector(remote_id):
|
def get_or_create_connector(remote_id):
|
||||||
''' get the connector related to the author's server '''
|
""" get the connector related to the author's server """
|
||||||
url = urlparse(remote_id)
|
url = urlparse(remote_id)
|
||||||
identifier = url.netloc
|
identifier = url.netloc
|
||||||
if not identifier:
|
if not identifier:
|
||||||
raise ValueError('Invalid remote id')
|
raise ValueError("Invalid remote id")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
connector_info = models.Connector.objects.get(identifier=identifier)
|
connector_info = models.Connector.objects.get(identifier=identifier)
|
||||||
except models.Connector.DoesNotExist:
|
except models.Connector.DoesNotExist:
|
||||||
connector_info = models.Connector.objects.create(
|
connector_info = models.Connector.objects.create(
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
connector_file='bookwyrm_connector',
|
connector_file="bookwyrm_connector",
|
||||||
base_url='https://%s' % identifier,
|
base_url="https://%s" % identifier,
|
||||||
books_url='https://%s/book' % identifier,
|
books_url="https://%s/book" % identifier,
|
||||||
covers_url='https://%s/images/covers' % identifier,
|
covers_url="https://%s/images/covers" % identifier,
|
||||||
search_url='https://%s/search?q=' % identifier,
|
search_url="https://%s/search?q=" % identifier,
|
||||||
priority=2
|
priority=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
return load_connector(connector_info)
|
return load_connector(connector_info)
|
||||||
|
@ -81,7 +107,7 @@ def get_or_create_connector(remote_id):
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def load_more_data(connector_id, book_id):
|
def load_more_data(connector_id, book_id):
|
||||||
''' background the work of getting all 10,000 editions of LoTR '''
|
""" background the work of getting all 10,000 editions of LoTR """
|
||||||
connector_info = models.Connector.objects.get(id=connector_id)
|
connector_info = models.Connector.objects.get(id=connector_id)
|
||||||
connector = load_connector(connector_info)
|
connector = load_connector(connector_info)
|
||||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||||
|
@ -89,8 +115,8 @@ def load_more_data(connector_id, book_id):
|
||||||
|
|
||||||
|
|
||||||
def load_connector(connector_info):
|
def load_connector(connector_info):
|
||||||
''' instantiate the connector class '''
|
""" instantiate the connector class """
|
||||||
connector = importlib.import_module(
|
connector = importlib.import_module(
|
||||||
'bookwyrm.connectors.%s' % connector_info.connector_file
|
"bookwyrm.connectors.%s" % connector_info.connector_file
|
||||||
)
|
)
|
||||||
return connector.Connector(connector_info.identifier)
|
return connector.Connector(connector_info.identifier)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' openlibrary data connector '''
|
""" openlibrary data connector """
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
@ -9,132 +9,134 @@ from .openlibrary_languages import languages
|
||||||
|
|
||||||
|
|
||||||
class Connector(AbstractConnector):
|
class Connector(AbstractConnector):
|
||||||
''' instantiate a connector for OL '''
|
""" instantiate a connector for OL """
|
||||||
|
|
||||||
def __init__(self, identifier):
|
def __init__(self, identifier):
|
||||||
super().__init__(identifier)
|
super().__init__(identifier)
|
||||||
|
|
||||||
get_first = lambda a: a[0]
|
get_first = lambda a: a[0]
|
||||||
get_remote_id = lambda a: self.base_url + a
|
get_remote_id = lambda a: self.base_url + a
|
||||||
self.book_mappings = [
|
self.book_mappings = [
|
||||||
Mapping('title'),
|
Mapping("title"),
|
||||||
Mapping('id', remote_field='key', formatter=get_remote_id),
|
Mapping("id", remote_field="key", formatter=get_remote_id),
|
||||||
|
Mapping("cover", remote_field="covers", formatter=self.get_cover_url),
|
||||||
|
Mapping("sortTitle", remote_field="sort_title"),
|
||||||
|
Mapping("subtitle"),
|
||||||
|
Mapping("description", formatter=get_description),
|
||||||
|
Mapping("languages", formatter=get_languages),
|
||||||
|
Mapping("series", formatter=get_first),
|
||||||
|
Mapping("seriesNumber", remote_field="series_number"),
|
||||||
|
Mapping("subjects"),
|
||||||
|
Mapping("subjectPlaces", remote_field="subject_places"),
|
||||||
|
Mapping("isbn13", remote_field="isbn_13", formatter=get_first),
|
||||||
|
Mapping("isbn10", remote_field="isbn_10", formatter=get_first),
|
||||||
|
Mapping("lccn", formatter=get_first),
|
||||||
|
Mapping("oclcNumber", remote_field="oclc_numbers", formatter=get_first),
|
||||||
Mapping(
|
Mapping(
|
||||||
'cover', remote_field='covers', formatter=self.get_cover_url),
|
"openlibraryKey", remote_field="key", formatter=get_openlibrary_key
|
||||||
Mapping('sortTitle', remote_field='sort_title'),
|
|
||||||
Mapping('subtitle'),
|
|
||||||
Mapping('description', formatter=get_description),
|
|
||||||
Mapping('languages', formatter=get_languages),
|
|
||||||
Mapping('series', formatter=get_first),
|
|
||||||
Mapping('seriesNumber', remote_field='series_number'),
|
|
||||||
Mapping('subjects'),
|
|
||||||
Mapping('subjectPlaces', remote_field='subject_places'),
|
|
||||||
Mapping('isbn13', remote_field='isbn_13', formatter=get_first),
|
|
||||||
Mapping('isbn10', remote_field='isbn_10', formatter=get_first),
|
|
||||||
Mapping('lccn', formatter=get_first),
|
|
||||||
Mapping(
|
|
||||||
'oclcNumber', remote_field='oclc_numbers',
|
|
||||||
formatter=get_first
|
|
||||||
),
|
),
|
||||||
|
Mapping("goodreadsKey", remote_field="goodreads_key"),
|
||||||
|
Mapping("asin"),
|
||||||
Mapping(
|
Mapping(
|
||||||
'openlibraryKey', remote_field='key',
|
"firstPublishedDate",
|
||||||
formatter=get_openlibrary_key
|
remote_field="first_publish_date",
|
||||||
),
|
),
|
||||||
Mapping('goodreadsKey', remote_field='goodreads_key'),
|
Mapping("publishedDate", remote_field="publish_date"),
|
||||||
Mapping('asin'),
|
Mapping("pages", remote_field="number_of_pages"),
|
||||||
Mapping(
|
Mapping("physicalFormat", remote_field="physical_format"),
|
||||||
'firstPublishedDate', remote_field='first_publish_date',
|
Mapping("publishers"),
|
||||||
),
|
|
||||||
Mapping('publishedDate', remote_field='publish_date'),
|
|
||||||
Mapping('pages', remote_field='number_of_pages'),
|
|
||||||
Mapping('physicalFormat', remote_field='physical_format'),
|
|
||||||
Mapping('publishers'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
self.author_mappings = [
|
self.author_mappings = [
|
||||||
Mapping('id', remote_field='key', formatter=get_remote_id),
|
Mapping("id", remote_field="key", formatter=get_remote_id),
|
||||||
Mapping('name'),
|
Mapping("name"),
|
||||||
Mapping(
|
Mapping(
|
||||||
'openlibraryKey', remote_field='key',
|
"openlibraryKey", remote_field="key", formatter=get_openlibrary_key
|
||||||
formatter=get_openlibrary_key
|
|
||||||
),
|
),
|
||||||
Mapping('born', remote_field='birth_date'),
|
Mapping("born", remote_field="birth_date"),
|
||||||
Mapping('died', remote_field='death_date'),
|
Mapping("died", remote_field="death_date"),
|
||||||
Mapping('bio', formatter=get_description),
|
Mapping("bio", formatter=get_description),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_remote_id_from_data(self, data):
|
def get_remote_id_from_data(self, data):
|
||||||
''' format a url from an openlibrary id field '''
|
""" format a url from an openlibrary id field """
|
||||||
try:
|
try:
|
||||||
key = data['key']
|
key = data["key"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConnectorException('Invalid book data')
|
raise ConnectorException("Invalid book data")
|
||||||
return '%s%s' % (self.books_url, key)
|
return "%s%s" % (self.books_url, key)
|
||||||
|
|
||||||
|
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
return bool(re.match(r'^[\/\w]+OL\d+W$', data['key']))
|
return bool(re.match(r"^[\/\w]+OL\d+W$", data["key"]))
|
||||||
|
|
||||||
|
|
||||||
def get_edition_from_work_data(self, data):
|
def get_edition_from_work_data(self, data):
|
||||||
try:
|
try:
|
||||||
key = data['key']
|
key = data["key"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConnectorException('Invalid book data')
|
raise ConnectorException("Invalid book data")
|
||||||
url = '%s%s/editions' % (self.books_url, key)
|
url = "%s%s/editions" % (self.books_url, key)
|
||||||
data = get_data(url)
|
data = get_data(url)
|
||||||
return pick_default_edition(data['entries'])
|
return pick_default_edition(data["entries"])
|
||||||
|
|
||||||
|
|
||||||
def get_work_from_edition_data(self, data):
|
def get_work_from_edition_data(self, data):
|
||||||
try:
|
try:
|
||||||
key = data['works'][0]['key']
|
key = data["works"][0]["key"]
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
raise ConnectorException('No work found for edition')
|
raise ConnectorException("No work found for edition")
|
||||||
url = '%s%s' % (self.books_url, key)
|
url = "%s%s" % (self.books_url, key)
|
||||||
return get_data(url)
|
return get_data(url)
|
||||||
|
|
||||||
|
|
||||||
def get_authors_from_data(self, data):
|
def get_authors_from_data(self, data):
|
||||||
''' parse author json and load or create authors '''
|
""" parse author json and load or create authors """
|
||||||
for author_blob in data.get('authors', []):
|
for author_blob in data.get("authors", []):
|
||||||
author_blob = author_blob.get('author', author_blob)
|
author_blob = author_blob.get("author", author_blob)
|
||||||
# this id is "/authors/OL1234567A"
|
# this id is "/authors/OL1234567A"
|
||||||
author_id = author_blob['key']
|
author_id = author_blob["key"]
|
||||||
url = '%s%s' % (self.base_url, author_id)
|
url = "%s%s" % (self.base_url, author_id)
|
||||||
yield self.get_or_create_author(url)
|
yield self.get_or_create_author(url)
|
||||||
|
|
||||||
|
|
||||||
def get_cover_url(self, cover_blob):
|
def get_cover_url(self, cover_blob):
|
||||||
''' ask openlibrary for the cover '''
|
""" ask openlibrary for the cover """
|
||||||
cover_id = cover_blob[0]
|
cover_id = cover_blob[0]
|
||||||
image_name = '%s-L.jpg' % cover_id
|
image_name = "%s-L.jpg" % cover_id
|
||||||
return '%s/b/id/%s' % (self.covers_url, image_name)
|
return "%s/b/id/%s" % (self.covers_url, image_name)
|
||||||
|
|
||||||
|
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
return data.get('docs')
|
return data.get("docs")
|
||||||
|
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
# build the remote id from the openlibrary key
|
# build the remote id from the openlibrary key
|
||||||
key = self.books_url + search_result['key']
|
key = self.books_url + search_result["key"]
|
||||||
author = search_result.get('author_name') or ['Unknown']
|
author = search_result.get("author_name") or ["Unknown"]
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
title=search_result.get('title'),
|
title=search_result.get("title"),
|
||||||
key=key,
|
key=key,
|
||||||
author=', '.join(author),
|
author=", ".join(author),
|
||||||
connector=self,
|
connector=self,
|
||||||
year=search_result.get('first_publish_year'),
|
year=search_result.get("first_publish_year"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
return list(data.values())
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
# build the remote id from the openlibrary key
|
||||||
|
key = self.books_url + search_result["key"]
|
||||||
|
authors = search_result.get("authors") or [{"name": "Unknown"}]
|
||||||
|
author_names = [author.get("name") for author in authors]
|
||||||
|
return SearchResult(
|
||||||
|
title=search_result.get("title"),
|
||||||
|
key=key,
|
||||||
|
author=", ".join(author_names),
|
||||||
|
connector=self,
|
||||||
|
year=search_result.get("publish_date"),
|
||||||
|
)
|
||||||
|
|
||||||
def load_edition_data(self, olkey):
|
def load_edition_data(self, olkey):
|
||||||
''' query openlibrary for editions of a work '''
|
""" query openlibrary for editions of a work """
|
||||||
url = '%s/works/%s/editions' % (self.books_url, olkey)
|
url = "%s/works/%s/editions" % (self.books_url, olkey)
|
||||||
return get_data(url)
|
return get_data(url)
|
||||||
|
|
||||||
|
|
||||||
def expand_book_data(self, book):
|
def expand_book_data(self, book):
|
||||||
work = book
|
work = book
|
||||||
# go from the edition to the work, if necessary
|
# go from the edition to the work, if necessary
|
||||||
|
@ -148,7 +150,7 @@ class Connector(AbstractConnector):
|
||||||
# who knows, man
|
# who knows, man
|
||||||
return
|
return
|
||||||
|
|
||||||
for edition_data in edition_options.get('entries'):
|
for edition_data in edition_options.get("entries"):
|
||||||
# does this edition have ANY interesting data?
|
# does this edition have ANY interesting data?
|
||||||
if ignore_edition(edition_data):
|
if ignore_edition(edition_data):
|
||||||
continue
|
continue
|
||||||
|
@ -156,62 +158,63 @@ class Connector(AbstractConnector):
|
||||||
|
|
||||||
|
|
||||||
def ignore_edition(edition_data):
|
def ignore_edition(edition_data):
|
||||||
''' don't load a million editions that have no metadata '''
|
""" don't load a million editions that have no metadata """
|
||||||
# an isbn, we love to see it
|
# an isbn, we love to see it
|
||||||
if edition_data.get('isbn_13') or edition_data.get('isbn_10'):
|
if edition_data.get("isbn_13") or edition_data.get("isbn_10"):
|
||||||
print(edition_data.get('isbn_10'))
|
print(edition_data.get("isbn_10"))
|
||||||
return False
|
return False
|
||||||
# grudgingly, oclc can stay
|
# grudgingly, oclc can stay
|
||||||
if edition_data.get('oclc_numbers'):
|
if edition_data.get("oclc_numbers"):
|
||||||
print(edition_data.get('oclc_numbers'))
|
print(edition_data.get("oclc_numbers"))
|
||||||
return False
|
return False
|
||||||
# if it has a cover it can stay
|
# if it has a cover it can stay
|
||||||
if edition_data.get('covers'):
|
if edition_data.get("covers"):
|
||||||
print(edition_data.get('covers'))
|
print(edition_data.get("covers"))
|
||||||
return False
|
return False
|
||||||
# keep non-english editions
|
# keep non-english editions
|
||||||
if edition_data.get('languages') and \
|
if edition_data.get("languages") and "languages/eng" not in str(
|
||||||
'languages/eng' not in str(edition_data.get('languages')):
|
edition_data.get("languages")
|
||||||
print(edition_data.get('languages'))
|
):
|
||||||
|
print(edition_data.get("languages"))
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_description(description_blob):
|
def get_description(description_blob):
|
||||||
''' descriptions can be a string or a dict '''
|
""" descriptions can be a string or a dict """
|
||||||
if isinstance(description_blob, dict):
|
if isinstance(description_blob, dict):
|
||||||
return description_blob.get('value')
|
return description_blob.get("value")
|
||||||
return description_blob
|
return description_blob
|
||||||
|
|
||||||
|
|
||||||
def get_openlibrary_key(key):
|
def get_openlibrary_key(key):
|
||||||
''' convert /books/OL27320736M into OL27320736M '''
|
""" convert /books/OL27320736M into OL27320736M """
|
||||||
return key.split('/')[-1]
|
return key.split("/")[-1]
|
||||||
|
|
||||||
|
|
||||||
def get_languages(language_blob):
|
def get_languages(language_blob):
|
||||||
''' /language/eng -> English '''
|
""" /language/eng -> English """
|
||||||
langs = []
|
langs = []
|
||||||
for lang in language_blob:
|
for lang in language_blob:
|
||||||
langs.append(
|
langs.append(languages.get(lang.get("key", ""), None))
|
||||||
languages.get(lang.get('key', ''), None)
|
|
||||||
)
|
|
||||||
return langs
|
return langs
|
||||||
|
|
||||||
|
|
||||||
def pick_default_edition(options):
|
def pick_default_edition(options):
|
||||||
''' favor physical copies with covers in english '''
|
""" favor physical copies with covers in english """
|
||||||
if not options:
|
if not options:
|
||||||
return None
|
return None
|
||||||
if len(options) == 1:
|
if len(options) == 1:
|
||||||
return options[0]
|
return options[0]
|
||||||
|
|
||||||
options = [e for e in options if e.get('covers')] or options
|
options = [e for e in options if e.get("covers")] or options
|
||||||
options = [e for e in options if \
|
options = [
|
||||||
'/languages/eng' in str(e.get('languages'))] or options
|
e for e in options if "/languages/eng" in str(e.get("languages"))
|
||||||
formats = ['paperback', 'hardcover', 'mass market paperback']
|
] or options
|
||||||
options = [e for e in options if \
|
formats = ["paperback", "hardcover", "mass market paperback"]
|
||||||
str(e.get('physical_format')).lower() in formats] or options
|
options = [
|
||||||
options = [e for e in options if e.get('isbn_13')] or options
|
e for e in options if str(e.get("physical_format")).lower() in formats
|
||||||
options = [e for e in options if e.get('ocaid')] or options
|
] or options
|
||||||
|
options = [e for e in options if e.get("isbn_13")] or options
|
||||||
|
options = [e for e in options if e.get("ocaid")] or options
|
||||||
return options[0]
|
return options[0]
|
||||||
|
|
|
@ -1,467 +1,467 @@
|
||||||
''' key lookups for openlibrary languages '''
|
""" key lookups for openlibrary languages """
|
||||||
languages = {
|
languages = {
|
||||||
'/languages/eng': 'English',
|
"/languages/eng": "English",
|
||||||
'/languages/fre': 'French',
|
"/languages/fre": "French",
|
||||||
'/languages/spa': 'Spanish',
|
"/languages/spa": "Spanish",
|
||||||
'/languages/ger': 'German',
|
"/languages/ger": "German",
|
||||||
'/languages/rus': 'Russian',
|
"/languages/rus": "Russian",
|
||||||
'/languages/ita': 'Italian',
|
"/languages/ita": "Italian",
|
||||||
'/languages/chi': 'Chinese',
|
"/languages/chi": "Chinese",
|
||||||
'/languages/jpn': 'Japanese',
|
"/languages/jpn": "Japanese",
|
||||||
'/languages/por': 'Portuguese',
|
"/languages/por": "Portuguese",
|
||||||
'/languages/ara': 'Arabic',
|
"/languages/ara": "Arabic",
|
||||||
'/languages/pol': 'Polish',
|
"/languages/pol": "Polish",
|
||||||
'/languages/heb': 'Hebrew',
|
"/languages/heb": "Hebrew",
|
||||||
'/languages/kor': 'Korean',
|
"/languages/kor": "Korean",
|
||||||
'/languages/dut': 'Dutch',
|
"/languages/dut": "Dutch",
|
||||||
'/languages/ind': 'Indonesian',
|
"/languages/ind": "Indonesian",
|
||||||
'/languages/lat': 'Latin',
|
"/languages/lat": "Latin",
|
||||||
'/languages/und': 'Undetermined',
|
"/languages/und": "Undetermined",
|
||||||
'/languages/cmn': 'Mandarin',
|
"/languages/cmn": "Mandarin",
|
||||||
'/languages/hin': 'Hindi',
|
"/languages/hin": "Hindi",
|
||||||
'/languages/swe': 'Swedish',
|
"/languages/swe": "Swedish",
|
||||||
'/languages/dan': 'Danish',
|
"/languages/dan": "Danish",
|
||||||
'/languages/urd': 'Urdu',
|
"/languages/urd": "Urdu",
|
||||||
'/languages/hun': 'Hungarian',
|
"/languages/hun": "Hungarian",
|
||||||
'/languages/cze': 'Czech',
|
"/languages/cze": "Czech",
|
||||||
'/languages/tur': 'Turkish',
|
"/languages/tur": "Turkish",
|
||||||
'/languages/ukr': 'Ukrainian',
|
"/languages/ukr": "Ukrainian",
|
||||||
'/languages/gre': 'Greek',
|
"/languages/gre": "Greek",
|
||||||
'/languages/vie': 'Vietnamese',
|
"/languages/vie": "Vietnamese",
|
||||||
'/languages/bul': 'Bulgarian',
|
"/languages/bul": "Bulgarian",
|
||||||
'/languages/ben': 'Bengali',
|
"/languages/ben": "Bengali",
|
||||||
'/languages/rum': 'Romanian',
|
"/languages/rum": "Romanian",
|
||||||
'/languages/cat': 'Catalan',
|
"/languages/cat": "Catalan",
|
||||||
'/languages/nor': 'Norwegian',
|
"/languages/nor": "Norwegian",
|
||||||
'/languages/tha': 'Thai',
|
"/languages/tha": "Thai",
|
||||||
'/languages/per': 'Persian',
|
"/languages/per": "Persian",
|
||||||
'/languages/scr': 'Croatian',
|
"/languages/scr": "Croatian",
|
||||||
'/languages/mul': 'Multiple languages',
|
"/languages/mul": "Multiple languages",
|
||||||
'/languages/fin': 'Finnish',
|
"/languages/fin": "Finnish",
|
||||||
'/languages/tam': 'Tamil',
|
"/languages/tam": "Tamil",
|
||||||
'/languages/guj': 'Gujarati',
|
"/languages/guj": "Gujarati",
|
||||||
'/languages/mar': 'Marathi',
|
"/languages/mar": "Marathi",
|
||||||
'/languages/scc': 'Serbian',
|
"/languages/scc": "Serbian",
|
||||||
'/languages/pan': 'Panjabi',
|
"/languages/pan": "Panjabi",
|
||||||
'/languages/wel': 'Welsh',
|
"/languages/wel": "Welsh",
|
||||||
'/languages/tel': 'Telugu',
|
"/languages/tel": "Telugu",
|
||||||
'/languages/yid': 'Yiddish',
|
"/languages/yid": "Yiddish",
|
||||||
'/languages/kan': 'Kannada',
|
"/languages/kan": "Kannada",
|
||||||
'/languages/slo': 'Slovak',
|
"/languages/slo": "Slovak",
|
||||||
'/languages/san': 'Sanskrit',
|
"/languages/san": "Sanskrit",
|
||||||
'/languages/arm': 'Armenian',
|
"/languages/arm": "Armenian",
|
||||||
'/languages/mal': 'Malayalam',
|
"/languages/mal": "Malayalam",
|
||||||
'/languages/may': 'Malay',
|
"/languages/may": "Malay",
|
||||||
'/languages/bur': 'Burmese',
|
"/languages/bur": "Burmese",
|
||||||
'/languages/slv': 'Slovenian',
|
"/languages/slv": "Slovenian",
|
||||||
'/languages/lit': 'Lithuanian',
|
"/languages/lit": "Lithuanian",
|
||||||
'/languages/tib': 'Tibetan',
|
"/languages/tib": "Tibetan",
|
||||||
'/languages/lav': 'Latvian',
|
"/languages/lav": "Latvian",
|
||||||
'/languages/est': 'Estonian',
|
"/languages/est": "Estonian",
|
||||||
'/languages/nep': 'Nepali',
|
"/languages/nep": "Nepali",
|
||||||
'/languages/ori': 'Oriya',
|
"/languages/ori": "Oriya",
|
||||||
'/languages/mon': 'Mongolian',
|
"/languages/mon": "Mongolian",
|
||||||
'/languages/alb': 'Albanian',
|
"/languages/alb": "Albanian",
|
||||||
'/languages/iri': 'Irish',
|
"/languages/iri": "Irish",
|
||||||
'/languages/geo': 'Georgian',
|
"/languages/geo": "Georgian",
|
||||||
'/languages/afr': 'Afrikaans',
|
"/languages/afr": "Afrikaans",
|
||||||
'/languages/grc': 'Ancient Greek',
|
"/languages/grc": "Ancient Greek",
|
||||||
'/languages/mac': 'Macedonian',
|
"/languages/mac": "Macedonian",
|
||||||
'/languages/bel': 'Belarusian',
|
"/languages/bel": "Belarusian",
|
||||||
'/languages/ice': 'Icelandic',
|
"/languages/ice": "Icelandic",
|
||||||
'/languages/srp': 'Serbian',
|
"/languages/srp": "Serbian",
|
||||||
'/languages/snh': 'Sinhalese',
|
"/languages/snh": "Sinhalese",
|
||||||
'/languages/snd': 'Sindhi',
|
"/languages/snd": "Sindhi",
|
||||||
'/languages/ota': 'Turkish, Ottoman',
|
"/languages/ota": "Turkish, Ottoman",
|
||||||
'/languages/kur': 'Kurdish',
|
"/languages/kur": "Kurdish",
|
||||||
'/languages/aze': 'Azerbaijani',
|
"/languages/aze": "Azerbaijani",
|
||||||
'/languages/pus': 'Pushto',
|
"/languages/pus": "Pushto",
|
||||||
'/languages/amh': 'Amharic',
|
"/languages/amh": "Amharic",
|
||||||
'/languages/gag': 'Galician',
|
"/languages/gag": "Galician",
|
||||||
'/languages/hrv': 'Croatian',
|
"/languages/hrv": "Croatian",
|
||||||
'/languages/sin': 'Sinhalese',
|
"/languages/sin": "Sinhalese",
|
||||||
'/languages/asm': 'Assamese',
|
"/languages/asm": "Assamese",
|
||||||
'/languages/uzb': 'Uzbek',
|
"/languages/uzb": "Uzbek",
|
||||||
'/languages/gae': 'Scottish Gaelix',
|
"/languages/gae": "Scottish Gaelix",
|
||||||
'/languages/kaz': 'Kazakh',
|
"/languages/kaz": "Kazakh",
|
||||||
'/languages/swa': 'Swahili',
|
"/languages/swa": "Swahili",
|
||||||
'/languages/bos': 'Bosnian',
|
"/languages/bos": "Bosnian",
|
||||||
'/languages/glg': 'Galician ',
|
"/languages/glg": "Galician ",
|
||||||
'/languages/baq': 'Basque',
|
"/languages/baq": "Basque",
|
||||||
'/languages/tgl': 'Tagalog',
|
"/languages/tgl": "Tagalog",
|
||||||
'/languages/raj': 'Rajasthani',
|
"/languages/raj": "Rajasthani",
|
||||||
'/languages/gle': 'Irish',
|
"/languages/gle": "Irish",
|
||||||
'/languages/lao': 'Lao',
|
"/languages/lao": "Lao",
|
||||||
'/languages/jav': 'Javanese',
|
"/languages/jav": "Javanese",
|
||||||
'/languages/mai': 'Maithili',
|
"/languages/mai": "Maithili",
|
||||||
'/languages/tgk': 'Tajik ',
|
"/languages/tgk": "Tajik ",
|
||||||
'/languages/khm': 'Khmer',
|
"/languages/khm": "Khmer",
|
||||||
'/languages/roh': 'Raeto-Romance',
|
"/languages/roh": "Raeto-Romance",
|
||||||
'/languages/kok': 'Konkani ',
|
"/languages/kok": "Konkani ",
|
||||||
'/languages/sit': 'Sino-Tibetan (Other)',
|
"/languages/sit": "Sino-Tibetan (Other)",
|
||||||
'/languages/mol': 'Moldavian',
|
"/languages/mol": "Moldavian",
|
||||||
'/languages/kir': 'Kyrgyz',
|
"/languages/kir": "Kyrgyz",
|
||||||
'/languages/new': 'Newari',
|
"/languages/new": "Newari",
|
||||||
'/languages/inc': 'Indic (Other)',
|
"/languages/inc": "Indic (Other)",
|
||||||
'/languages/frm': 'French, Middle (ca. 1300-1600)',
|
"/languages/frm": "French, Middle (ca. 1300-1600)",
|
||||||
'/languages/esp': 'Esperanto',
|
"/languages/esp": "Esperanto",
|
||||||
'/languages/hau': 'Hausa',
|
"/languages/hau": "Hausa",
|
||||||
'/languages/tag': 'Tagalog',
|
"/languages/tag": "Tagalog",
|
||||||
'/languages/tuk': 'Turkmen',
|
"/languages/tuk": "Turkmen",
|
||||||
'/languages/enm': 'English, Middle (1100-1500)',
|
"/languages/enm": "English, Middle (1100-1500)",
|
||||||
'/languages/map': 'Austronesian (Other)',
|
"/languages/map": "Austronesian (Other)",
|
||||||
'/languages/pli': 'Pali',
|
"/languages/pli": "Pali",
|
||||||
'/languages/fro': 'French, Old (ca. 842-1300)',
|
"/languages/fro": "French, Old (ca. 842-1300)",
|
||||||
'/languages/nic': 'Niger-Kordofanian (Other)',
|
"/languages/nic": "Niger-Kordofanian (Other)",
|
||||||
'/languages/tir': 'Tigrinya',
|
"/languages/tir": "Tigrinya",
|
||||||
'/languages/wen': 'Sorbian (Other)',
|
"/languages/wen": "Sorbian (Other)",
|
||||||
'/languages/bho': 'Bhojpuri',
|
"/languages/bho": "Bhojpuri",
|
||||||
'/languages/roa': 'Romance (Other)',
|
"/languages/roa": "Romance (Other)",
|
||||||
'/languages/tut': 'Altaic (Other)',
|
"/languages/tut": "Altaic (Other)",
|
||||||
'/languages/bra': 'Braj',
|
"/languages/bra": "Braj",
|
||||||
'/languages/sun': 'Sundanese',
|
"/languages/sun": "Sundanese",
|
||||||
'/languages/fiu': 'Finno-Ugrian (Other)',
|
"/languages/fiu": "Finno-Ugrian (Other)",
|
||||||
'/languages/far': 'Faroese',
|
"/languages/far": "Faroese",
|
||||||
'/languages/ban': 'Balinese',
|
"/languages/ban": "Balinese",
|
||||||
'/languages/tar': 'Tatar',
|
"/languages/tar": "Tatar",
|
||||||
'/languages/bak': 'Bashkir',
|
"/languages/bak": "Bashkir",
|
||||||
'/languages/tat': 'Tatar',
|
"/languages/tat": "Tatar",
|
||||||
'/languages/chu': 'Church Slavic',
|
"/languages/chu": "Church Slavic",
|
||||||
'/languages/dra': 'Dravidian (Other)',
|
"/languages/dra": "Dravidian (Other)",
|
||||||
'/languages/pra': 'Prakrit languages',
|
"/languages/pra": "Prakrit languages",
|
||||||
'/languages/paa': 'Papuan (Other)',
|
"/languages/paa": "Papuan (Other)",
|
||||||
'/languages/doi': 'Dogri',
|
"/languages/doi": "Dogri",
|
||||||
'/languages/lah': 'Lahndā',
|
"/languages/lah": "Lahndā",
|
||||||
'/languages/mni': 'Manipuri',
|
"/languages/mni": "Manipuri",
|
||||||
'/languages/yor': 'Yoruba',
|
"/languages/yor": "Yoruba",
|
||||||
'/languages/gmh': 'German, Middle High (ca. 1050-1500)',
|
"/languages/gmh": "German, Middle High (ca. 1050-1500)",
|
||||||
'/languages/kas': 'Kashmiri',
|
"/languages/kas": "Kashmiri",
|
||||||
'/languages/fri': 'Frisian',
|
"/languages/fri": "Frisian",
|
||||||
'/languages/mla': 'Malagasy',
|
"/languages/mla": "Malagasy",
|
||||||
'/languages/egy': 'Egyptian',
|
"/languages/egy": "Egyptian",
|
||||||
'/languages/rom': 'Romani',
|
"/languages/rom": "Romani",
|
||||||
'/languages/syr': 'Syriac, Modern',
|
"/languages/syr": "Syriac, Modern",
|
||||||
'/languages/cau': 'Caucasian (Other)',
|
"/languages/cau": "Caucasian (Other)",
|
||||||
'/languages/hbs': 'Serbo-Croatian',
|
"/languages/hbs": "Serbo-Croatian",
|
||||||
'/languages/sai': 'South American Indian (Other)',
|
"/languages/sai": "South American Indian (Other)",
|
||||||
'/languages/pro': 'Provençal (to 1500)',
|
"/languages/pro": "Provençal (to 1500)",
|
||||||
'/languages/cpf': 'Creoles and Pidgins, French-based (Other)',
|
"/languages/cpf": "Creoles and Pidgins, French-based (Other)",
|
||||||
'/languages/ang': 'English, Old (ca. 450-1100)',
|
"/languages/ang": "English, Old (ca. 450-1100)",
|
||||||
'/languages/bal': 'Baluchi',
|
"/languages/bal": "Baluchi",
|
||||||
'/languages/gla': 'Scottish Gaelic',
|
"/languages/gla": "Scottish Gaelic",
|
||||||
'/languages/chv': 'Chuvash',
|
"/languages/chv": "Chuvash",
|
||||||
'/languages/kin': 'Kinyarwanda',
|
"/languages/kin": "Kinyarwanda",
|
||||||
'/languages/zul': 'Zulu',
|
"/languages/zul": "Zulu",
|
||||||
'/languages/sla': 'Slavic (Other)',
|
"/languages/sla": "Slavic (Other)",
|
||||||
'/languages/som': 'Somali',
|
"/languages/som": "Somali",
|
||||||
'/languages/mlt': 'Maltese',
|
"/languages/mlt": "Maltese",
|
||||||
'/languages/uig': 'Uighur',
|
"/languages/uig": "Uighur",
|
||||||
'/languages/mlg': 'Malagasy',
|
"/languages/mlg": "Malagasy",
|
||||||
'/languages/sho': 'Shona',
|
"/languages/sho": "Shona",
|
||||||
'/languages/lan': 'Occitan (post 1500)',
|
"/languages/lan": "Occitan (post 1500)",
|
||||||
'/languages/bre': 'Breton',
|
"/languages/bre": "Breton",
|
||||||
'/languages/sco': 'Scots',
|
"/languages/sco": "Scots",
|
||||||
'/languages/sso': 'Sotho',
|
"/languages/sso": "Sotho",
|
||||||
'/languages/myn': 'Mayan languages',
|
"/languages/myn": "Mayan languages",
|
||||||
'/languages/xho': 'Xhosa',
|
"/languages/xho": "Xhosa",
|
||||||
'/languages/gem': 'Germanic (Other)',
|
"/languages/gem": "Germanic (Other)",
|
||||||
'/languages/esk': 'Eskimo languages',
|
"/languages/esk": "Eskimo languages",
|
||||||
'/languages/akk': 'Akkadian',
|
"/languages/akk": "Akkadian",
|
||||||
'/languages/div': 'Maldivian',
|
"/languages/div": "Maldivian",
|
||||||
'/languages/sah': 'Yakut',
|
"/languages/sah": "Yakut",
|
||||||
'/languages/tsw': 'Tswana',
|
"/languages/tsw": "Tswana",
|
||||||
'/languages/nso': 'Northern Sotho',
|
"/languages/nso": "Northern Sotho",
|
||||||
'/languages/pap': 'Papiamento',
|
"/languages/pap": "Papiamento",
|
||||||
'/languages/bnt': 'Bantu (Other)',
|
"/languages/bnt": "Bantu (Other)",
|
||||||
'/languages/oss': 'Ossetic',
|
"/languages/oss": "Ossetic",
|
||||||
'/languages/cre': 'Cree',
|
"/languages/cre": "Cree",
|
||||||
'/languages/ibo': 'Igbo',
|
"/languages/ibo": "Igbo",
|
||||||
'/languages/fao': 'Faroese',
|
"/languages/fao": "Faroese",
|
||||||
'/languages/nai': 'North American Indian (Other)',
|
"/languages/nai": "North American Indian (Other)",
|
||||||
'/languages/mag': 'Magahi',
|
"/languages/mag": "Magahi",
|
||||||
'/languages/arc': 'Aramaic',
|
"/languages/arc": "Aramaic",
|
||||||
'/languages/epo': 'Esperanto',
|
"/languages/epo": "Esperanto",
|
||||||
'/languages/kha': 'Khasi',
|
"/languages/kha": "Khasi",
|
||||||
'/languages/oji': 'Ojibwa',
|
"/languages/oji": "Ojibwa",
|
||||||
'/languages/que': 'Quechua',
|
"/languages/que": "Quechua",
|
||||||
'/languages/lug': 'Ganda',
|
"/languages/lug": "Ganda",
|
||||||
'/languages/mwr': 'Marwari',
|
"/languages/mwr": "Marwari",
|
||||||
'/languages/awa': 'Awadhi ',
|
"/languages/awa": "Awadhi ",
|
||||||
'/languages/cor': 'Cornish',
|
"/languages/cor": "Cornish",
|
||||||
'/languages/lad': 'Ladino',
|
"/languages/lad": "Ladino",
|
||||||
'/languages/dzo': 'Dzongkha',
|
"/languages/dzo": "Dzongkha",
|
||||||
'/languages/cop': 'Coptic',
|
"/languages/cop": "Coptic",
|
||||||
'/languages/nah': 'Nahuatl',
|
"/languages/nah": "Nahuatl",
|
||||||
'/languages/cai': 'Central American Indian (Other)',
|
"/languages/cai": "Central American Indian (Other)",
|
||||||
'/languages/phi': 'Philippine (Other)',
|
"/languages/phi": "Philippine (Other)",
|
||||||
'/languages/moh': 'Mohawk',
|
"/languages/moh": "Mohawk",
|
||||||
'/languages/crp': 'Creoles and Pidgins (Other)',
|
"/languages/crp": "Creoles and Pidgins (Other)",
|
||||||
'/languages/nya': 'Nyanja',
|
"/languages/nya": "Nyanja",
|
||||||
'/languages/wol': 'Wolof ',
|
"/languages/wol": "Wolof ",
|
||||||
'/languages/haw': 'Hawaiian',
|
"/languages/haw": "Hawaiian",
|
||||||
'/languages/eth': 'Ethiopic',
|
"/languages/eth": "Ethiopic",
|
||||||
'/languages/mis': 'Miscellaneous languages',
|
"/languages/mis": "Miscellaneous languages",
|
||||||
'/languages/mkh': 'Mon-Khmer (Other)',
|
"/languages/mkh": "Mon-Khmer (Other)",
|
||||||
'/languages/alg': 'Algonquian (Other)',
|
"/languages/alg": "Algonquian (Other)",
|
||||||
'/languages/nde': 'Ndebele (Zimbabwe)',
|
"/languages/nde": "Ndebele (Zimbabwe)",
|
||||||
'/languages/ssa': 'Nilo-Saharan (Other)',
|
"/languages/ssa": "Nilo-Saharan (Other)",
|
||||||
'/languages/chm': 'Mari',
|
"/languages/chm": "Mari",
|
||||||
'/languages/che': 'Chechen',
|
"/languages/che": "Chechen",
|
||||||
'/languages/gez': 'Ethiopic',
|
"/languages/gez": "Ethiopic",
|
||||||
'/languages/ven': 'Venda',
|
"/languages/ven": "Venda",
|
||||||
'/languages/cam': 'Khmer',
|
"/languages/cam": "Khmer",
|
||||||
'/languages/fur': 'Friulian',
|
"/languages/fur": "Friulian",
|
||||||
'/languages/ful': 'Fula',
|
"/languages/ful": "Fula",
|
||||||
'/languages/gal': 'Oromo',
|
"/languages/gal": "Oromo",
|
||||||
'/languages/jrb': 'Judeo-Arabic',
|
"/languages/jrb": "Judeo-Arabic",
|
||||||
'/languages/bua': 'Buriat',
|
"/languages/bua": "Buriat",
|
||||||
'/languages/ady': 'Adygei',
|
"/languages/ady": "Adygei",
|
||||||
'/languages/bem': 'Bemba',
|
"/languages/bem": "Bemba",
|
||||||
'/languages/kar': 'Karen languages',
|
"/languages/kar": "Karen languages",
|
||||||
'/languages/sna': 'Shona',
|
"/languages/sna": "Shona",
|
||||||
'/languages/twi': 'Twi',
|
"/languages/twi": "Twi",
|
||||||
'/languages/btk': 'Batak',
|
"/languages/btk": "Batak",
|
||||||
'/languages/kaa': 'Kara-Kalpak',
|
"/languages/kaa": "Kara-Kalpak",
|
||||||
'/languages/kom': 'Komi',
|
"/languages/kom": "Komi",
|
||||||
'/languages/sot': 'Sotho',
|
"/languages/sot": "Sotho",
|
||||||
'/languages/tso': 'Tsonga',
|
"/languages/tso": "Tsonga",
|
||||||
'/languages/cpe': 'Creoles and Pidgins, English-based (Other)',
|
"/languages/cpe": "Creoles and Pidgins, English-based (Other)",
|
||||||
'/languages/gua': 'Guarani',
|
"/languages/gua": "Guarani",
|
||||||
'/languages/mao': 'Maori',
|
"/languages/mao": "Maori",
|
||||||
'/languages/mic': 'Micmac',
|
"/languages/mic": "Micmac",
|
||||||
'/languages/swz': 'Swazi',
|
"/languages/swz": "Swazi",
|
||||||
'/languages/taj': 'Tajik',
|
"/languages/taj": "Tajik",
|
||||||
'/languages/smo': 'Samoan',
|
"/languages/smo": "Samoan",
|
||||||
'/languages/ace': 'Achinese',
|
"/languages/ace": "Achinese",
|
||||||
'/languages/afa': 'Afroasiatic (Other)',
|
"/languages/afa": "Afroasiatic (Other)",
|
||||||
'/languages/lap': 'Sami',
|
"/languages/lap": "Sami",
|
||||||
'/languages/min': 'Minangkabau',
|
"/languages/min": "Minangkabau",
|
||||||
'/languages/oci': 'Occitan (post 1500)',
|
"/languages/oci": "Occitan (post 1500)",
|
||||||
'/languages/tsn': 'Tswana',
|
"/languages/tsn": "Tswana",
|
||||||
'/languages/pal': 'Pahlavi',
|
"/languages/pal": "Pahlavi",
|
||||||
'/languages/sux': 'Sumerian',
|
"/languages/sux": "Sumerian",
|
||||||
'/languages/ewe': 'Ewe',
|
"/languages/ewe": "Ewe",
|
||||||
'/languages/him': 'Himachali',
|
"/languages/him": "Himachali",
|
||||||
'/languages/kaw': 'Kawi',
|
"/languages/kaw": "Kawi",
|
||||||
'/languages/lus': 'Lushai',
|
"/languages/lus": "Lushai",
|
||||||
'/languages/ceb': 'Cebuano',
|
"/languages/ceb": "Cebuano",
|
||||||
'/languages/chr': 'Cherokee',
|
"/languages/chr": "Cherokee",
|
||||||
'/languages/fil': 'Filipino',
|
"/languages/fil": "Filipino",
|
||||||
'/languages/ndo': 'Ndonga',
|
"/languages/ndo": "Ndonga",
|
||||||
'/languages/ilo': 'Iloko',
|
"/languages/ilo": "Iloko",
|
||||||
'/languages/kbd': 'Kabardian',
|
"/languages/kbd": "Kabardian",
|
||||||
'/languages/orm': 'Oromo',
|
"/languages/orm": "Oromo",
|
||||||
'/languages/dum': 'Dutch, Middle (ca. 1050-1350)',
|
"/languages/dum": "Dutch, Middle (ca. 1050-1350)",
|
||||||
'/languages/bam': 'Bambara',
|
"/languages/bam": "Bambara",
|
||||||
'/languages/goh': 'Old High German',
|
"/languages/goh": "Old High German",
|
||||||
'/languages/got': 'Gothic',
|
"/languages/got": "Gothic",
|
||||||
'/languages/kon': 'Kongo',
|
"/languages/kon": "Kongo",
|
||||||
'/languages/mun': 'Munda (Other)',
|
"/languages/mun": "Munda (Other)",
|
||||||
'/languages/kru': 'Kurukh',
|
"/languages/kru": "Kurukh",
|
||||||
'/languages/pam': 'Pampanga',
|
"/languages/pam": "Pampanga",
|
||||||
'/languages/grn': 'Guarani',
|
"/languages/grn": "Guarani",
|
||||||
'/languages/gaa': 'Gã',
|
"/languages/gaa": "Gã",
|
||||||
'/languages/fry': 'Frisian',
|
"/languages/fry": "Frisian",
|
||||||
'/languages/iba': 'Iban',
|
"/languages/iba": "Iban",
|
||||||
'/languages/mak': 'Makasar',
|
"/languages/mak": "Makasar",
|
||||||
'/languages/kik': 'Kikuyu',
|
"/languages/kik": "Kikuyu",
|
||||||
'/languages/cho': 'Choctaw',
|
"/languages/cho": "Choctaw",
|
||||||
'/languages/cpp': 'Creoles and Pidgins, Portuguese-based (Other)',
|
"/languages/cpp": "Creoles and Pidgins, Portuguese-based (Other)",
|
||||||
'/languages/dak': 'Dakota',
|
"/languages/dak": "Dakota",
|
||||||
'/languages/udm': 'Udmurt ',
|
"/languages/udm": "Udmurt ",
|
||||||
'/languages/hat': 'Haitian French Creole',
|
"/languages/hat": "Haitian French Creole",
|
||||||
'/languages/mus': 'Creek',
|
"/languages/mus": "Creek",
|
||||||
'/languages/ber': 'Berber (Other)',
|
"/languages/ber": "Berber (Other)",
|
||||||
'/languages/hil': 'Hiligaynon',
|
"/languages/hil": "Hiligaynon",
|
||||||
'/languages/iro': 'Iroquoian (Other)',
|
"/languages/iro": "Iroquoian (Other)",
|
||||||
'/languages/kua': 'Kuanyama',
|
"/languages/kua": "Kuanyama",
|
||||||
'/languages/mno': 'Manobo languages',
|
"/languages/mno": "Manobo languages",
|
||||||
'/languages/run': 'Rundi',
|
"/languages/run": "Rundi",
|
||||||
'/languages/sat': 'Santali',
|
"/languages/sat": "Santali",
|
||||||
'/languages/shn': 'Shan',
|
"/languages/shn": "Shan",
|
||||||
'/languages/tyv': 'Tuvinian',
|
"/languages/tyv": "Tuvinian",
|
||||||
'/languages/chg': 'Chagatai',
|
"/languages/chg": "Chagatai",
|
||||||
'/languages/syc': 'Syriac',
|
"/languages/syc": "Syriac",
|
||||||
'/languages/ath': 'Athapascan (Other)',
|
"/languages/ath": "Athapascan (Other)",
|
||||||
'/languages/aym': 'Aymara',
|
"/languages/aym": "Aymara",
|
||||||
'/languages/bug': 'Bugis',
|
"/languages/bug": "Bugis",
|
||||||
'/languages/cel': 'Celtic (Other)',
|
"/languages/cel": "Celtic (Other)",
|
||||||
'/languages/int': 'Interlingua (International Auxiliary Language Association)',
|
"/languages/int": "Interlingua (International Auxiliary Language Association)",
|
||||||
'/languages/xal': 'Oirat',
|
"/languages/xal": "Oirat",
|
||||||
'/languages/ava': 'Avaric',
|
"/languages/ava": "Avaric",
|
||||||
'/languages/son': 'Songhai',
|
"/languages/son": "Songhai",
|
||||||
'/languages/tah': 'Tahitian',
|
"/languages/tah": "Tahitian",
|
||||||
'/languages/tet': 'Tetum',
|
"/languages/tet": "Tetum",
|
||||||
'/languages/ira': 'Iranian (Other)',
|
"/languages/ira": "Iranian (Other)",
|
||||||
'/languages/kac': 'Kachin',
|
"/languages/kac": "Kachin",
|
||||||
'/languages/nob': 'Norwegian (Bokmål)',
|
"/languages/nob": "Norwegian (Bokmål)",
|
||||||
'/languages/vai': 'Vai',
|
"/languages/vai": "Vai",
|
||||||
'/languages/bik': 'Bikol',
|
"/languages/bik": "Bikol",
|
||||||
'/languages/mos': 'Mooré',
|
"/languages/mos": "Mooré",
|
||||||
'/languages/tig': 'Tigré',
|
"/languages/tig": "Tigré",
|
||||||
'/languages/fat': 'Fanti',
|
"/languages/fat": "Fanti",
|
||||||
'/languages/her': 'Herero',
|
"/languages/her": "Herero",
|
||||||
'/languages/kal': 'Kalâtdlisut',
|
"/languages/kal": "Kalâtdlisut",
|
||||||
'/languages/mad': 'Madurese',
|
"/languages/mad": "Madurese",
|
||||||
'/languages/yue': 'Cantonese',
|
"/languages/yue": "Cantonese",
|
||||||
'/languages/chn': 'Chinook jargon',
|
"/languages/chn": "Chinook jargon",
|
||||||
'/languages/hmn': 'Hmong',
|
"/languages/hmn": "Hmong",
|
||||||
'/languages/lin': 'Lingala',
|
"/languages/lin": "Lingala",
|
||||||
'/languages/man': 'Mandingo',
|
"/languages/man": "Mandingo",
|
||||||
'/languages/nds': 'Low German',
|
"/languages/nds": "Low German",
|
||||||
'/languages/bas': 'Basa',
|
"/languages/bas": "Basa",
|
||||||
'/languages/gay': 'Gayo',
|
"/languages/gay": "Gayo",
|
||||||
'/languages/gsw': 'gsw',
|
"/languages/gsw": "gsw",
|
||||||
'/languages/ine': 'Indo-European (Other)',
|
"/languages/ine": "Indo-European (Other)",
|
||||||
'/languages/kro': 'Kru (Other)',
|
"/languages/kro": "Kru (Other)",
|
||||||
'/languages/kum': 'Kumyk',
|
"/languages/kum": "Kumyk",
|
||||||
'/languages/tsi': 'Tsimshian',
|
"/languages/tsi": "Tsimshian",
|
||||||
'/languages/zap': 'Zapotec',
|
"/languages/zap": "Zapotec",
|
||||||
'/languages/ach': 'Acoli',
|
"/languages/ach": "Acoli",
|
||||||
'/languages/ada': 'Adangme',
|
"/languages/ada": "Adangme",
|
||||||
'/languages/aka': 'Akan',
|
"/languages/aka": "Akan",
|
||||||
'/languages/khi': 'Khoisan (Other)',
|
"/languages/khi": "Khoisan (Other)",
|
||||||
'/languages/srd': 'Sardinian',
|
"/languages/srd": "Sardinian",
|
||||||
'/languages/arn': 'Mapuche',
|
"/languages/arn": "Mapuche",
|
||||||
'/languages/dyu': 'Dyula',
|
"/languages/dyu": "Dyula",
|
||||||
'/languages/loz': 'Lozi',
|
"/languages/loz": "Lozi",
|
||||||
'/languages/ltz': 'Luxembourgish',
|
"/languages/ltz": "Luxembourgish",
|
||||||
'/languages/sag': 'Sango (Ubangi Creole)',
|
"/languages/sag": "Sango (Ubangi Creole)",
|
||||||
'/languages/lez': 'Lezgian',
|
"/languages/lez": "Lezgian",
|
||||||
'/languages/luo': 'Luo (Kenya and Tanzania)',
|
"/languages/luo": "Luo (Kenya and Tanzania)",
|
||||||
'/languages/ssw': 'Swazi ',
|
"/languages/ssw": "Swazi ",
|
||||||
'/languages/krc': 'Karachay-Balkar',
|
"/languages/krc": "Karachay-Balkar",
|
||||||
'/languages/nyn': 'Nyankole',
|
"/languages/nyn": "Nyankole",
|
||||||
'/languages/sal': 'Salishan languages',
|
"/languages/sal": "Salishan languages",
|
||||||
'/languages/jpr': 'Judeo-Persian',
|
"/languages/jpr": "Judeo-Persian",
|
||||||
'/languages/pau': 'Palauan',
|
"/languages/pau": "Palauan",
|
||||||
'/languages/smi': 'Sami',
|
"/languages/smi": "Sami",
|
||||||
'/languages/aar': 'Afar',
|
"/languages/aar": "Afar",
|
||||||
'/languages/abk': 'Abkhaz',
|
"/languages/abk": "Abkhaz",
|
||||||
'/languages/gon': 'Gondi',
|
"/languages/gon": "Gondi",
|
||||||
'/languages/nzi': 'Nzima',
|
"/languages/nzi": "Nzima",
|
||||||
'/languages/sam': 'Samaritan Aramaic',
|
"/languages/sam": "Samaritan Aramaic",
|
||||||
'/languages/sao': 'Samoan',
|
"/languages/sao": "Samoan",
|
||||||
'/languages/srr': 'Serer',
|
"/languages/srr": "Serer",
|
||||||
'/languages/apa': 'Apache languages',
|
"/languages/apa": "Apache languages",
|
||||||
'/languages/crh': 'Crimean Tatar',
|
"/languages/crh": "Crimean Tatar",
|
||||||
'/languages/efi': 'Efik',
|
"/languages/efi": "Efik",
|
||||||
'/languages/iku': 'Inuktitut',
|
"/languages/iku": "Inuktitut",
|
||||||
'/languages/nav': 'Navajo',
|
"/languages/nav": "Navajo",
|
||||||
'/languages/pon': 'Ponape',
|
"/languages/pon": "Ponape",
|
||||||
'/languages/tmh': 'Tamashek',
|
"/languages/tmh": "Tamashek",
|
||||||
'/languages/aus': 'Australian languages',
|
"/languages/aus": "Australian languages",
|
||||||
'/languages/oto': 'Otomian languages',
|
"/languages/oto": "Otomian languages",
|
||||||
'/languages/war': 'Waray',
|
"/languages/war": "Waray",
|
||||||
'/languages/ypk': 'Yupik languages',
|
"/languages/ypk": "Yupik languages",
|
||||||
'/languages/ave': 'Avestan',
|
"/languages/ave": "Avestan",
|
||||||
'/languages/cus': 'Cushitic (Other)',
|
"/languages/cus": "Cushitic (Other)",
|
||||||
'/languages/del': 'Delaware',
|
"/languages/del": "Delaware",
|
||||||
'/languages/fon': 'Fon',
|
"/languages/fon": "Fon",
|
||||||
'/languages/ina': 'Interlingua (International Auxiliary Language Association)',
|
"/languages/ina": "Interlingua (International Auxiliary Language Association)",
|
||||||
'/languages/myv': 'Erzya',
|
"/languages/myv": "Erzya",
|
||||||
'/languages/pag': 'Pangasinan',
|
"/languages/pag": "Pangasinan",
|
||||||
'/languages/peo': 'Old Persian (ca. 600-400 B.C.)',
|
"/languages/peo": "Old Persian (ca. 600-400 B.C.)",
|
||||||
'/languages/vls': 'Flemish',
|
"/languages/vls": "Flemish",
|
||||||
'/languages/bai': 'Bamileke languages',
|
"/languages/bai": "Bamileke languages",
|
||||||
'/languages/bla': 'Siksika',
|
"/languages/bla": "Siksika",
|
||||||
'/languages/day': 'Dayak',
|
"/languages/day": "Dayak",
|
||||||
'/languages/men': 'Mende',
|
"/languages/men": "Mende",
|
||||||
'/languages/tai': 'Tai',
|
"/languages/tai": "Tai",
|
||||||
'/languages/ton': 'Tongan',
|
"/languages/ton": "Tongan",
|
||||||
'/languages/uga': 'Ugaritic',
|
"/languages/uga": "Ugaritic",
|
||||||
'/languages/yao': 'Yao (Africa)',
|
"/languages/yao": "Yao (Africa)",
|
||||||
'/languages/zza': 'Zaza',
|
"/languages/zza": "Zaza",
|
||||||
'/languages/bin': 'Edo',
|
"/languages/bin": "Edo",
|
||||||
'/languages/frs': 'East Frisian',
|
"/languages/frs": "East Frisian",
|
||||||
'/languages/inh': 'Ingush',
|
"/languages/inh": "Ingush",
|
||||||
'/languages/mah': 'Marshallese',
|
"/languages/mah": "Marshallese",
|
||||||
'/languages/sem': 'Semitic (Other)',
|
"/languages/sem": "Semitic (Other)",
|
||||||
'/languages/art': 'Artificial (Other)',
|
"/languages/art": "Artificial (Other)",
|
||||||
'/languages/chy': 'Cheyenne',
|
"/languages/chy": "Cheyenne",
|
||||||
'/languages/cmc': 'Chamic languages',
|
"/languages/cmc": "Chamic languages",
|
||||||
'/languages/dar': 'Dargwa',
|
"/languages/dar": "Dargwa",
|
||||||
'/languages/dua': 'Duala',
|
"/languages/dua": "Duala",
|
||||||
'/languages/elx': 'Elamite',
|
"/languages/elx": "Elamite",
|
||||||
'/languages/fan': 'Fang',
|
"/languages/fan": "Fang",
|
||||||
'/languages/fij': 'Fijian',
|
"/languages/fij": "Fijian",
|
||||||
'/languages/gil': 'Gilbertese',
|
"/languages/gil": "Gilbertese",
|
||||||
'/languages/ijo': 'Ijo',
|
"/languages/ijo": "Ijo",
|
||||||
'/languages/kam': 'Kamba',
|
"/languages/kam": "Kamba",
|
||||||
'/languages/nog': 'Nogai',
|
"/languages/nog": "Nogai",
|
||||||
'/languages/non': 'Old Norse',
|
"/languages/non": "Old Norse",
|
||||||
'/languages/tem': 'Temne',
|
"/languages/tem": "Temne",
|
||||||
'/languages/arg': 'Aragonese',
|
"/languages/arg": "Aragonese",
|
||||||
'/languages/arp': 'Arapaho',
|
"/languages/arp": "Arapaho",
|
||||||
'/languages/arw': 'Arawak',
|
"/languages/arw": "Arawak",
|
||||||
'/languages/din': 'Dinka',
|
"/languages/din": "Dinka",
|
||||||
'/languages/grb': 'Grebo',
|
"/languages/grb": "Grebo",
|
||||||
'/languages/kos': 'Kusaie',
|
"/languages/kos": "Kusaie",
|
||||||
'/languages/lub': 'Luba-Katanga',
|
"/languages/lub": "Luba-Katanga",
|
||||||
'/languages/mnc': 'Manchu',
|
"/languages/mnc": "Manchu",
|
||||||
'/languages/nyo': 'Nyoro',
|
"/languages/nyo": "Nyoro",
|
||||||
'/languages/rar': 'Rarotongan',
|
"/languages/rar": "Rarotongan",
|
||||||
'/languages/sel': 'Selkup',
|
"/languages/sel": "Selkup",
|
||||||
'/languages/tkl': 'Tokelauan',
|
"/languages/tkl": "Tokelauan",
|
||||||
'/languages/tog': 'Tonga (Nyasa)',
|
"/languages/tog": "Tonga (Nyasa)",
|
||||||
'/languages/tum': 'Tumbuka',
|
"/languages/tum": "Tumbuka",
|
||||||
'/languages/alt': 'Altai',
|
"/languages/alt": "Altai",
|
||||||
'/languages/ase': 'American Sign Language',
|
"/languages/ase": "American Sign Language",
|
||||||
'/languages/ast': 'Asturian',
|
"/languages/ast": "Asturian",
|
||||||
'/languages/chk': 'Chuukese',
|
"/languages/chk": "Chuukese",
|
||||||
'/languages/cos': 'Corsican',
|
"/languages/cos": "Corsican",
|
||||||
'/languages/ewo': 'Ewondo',
|
"/languages/ewo": "Ewondo",
|
||||||
'/languages/gor': 'Gorontalo',
|
"/languages/gor": "Gorontalo",
|
||||||
'/languages/hmo': 'Hiri Motu',
|
"/languages/hmo": "Hiri Motu",
|
||||||
'/languages/lol': 'Mongo-Nkundu',
|
"/languages/lol": "Mongo-Nkundu",
|
||||||
'/languages/lun': 'Lunda',
|
"/languages/lun": "Lunda",
|
||||||
'/languages/mas': 'Masai',
|
"/languages/mas": "Masai",
|
||||||
'/languages/niu': 'Niuean',
|
"/languages/niu": "Niuean",
|
||||||
'/languages/rup': 'Aromanian',
|
"/languages/rup": "Aromanian",
|
||||||
'/languages/sas': 'Sasak',
|
"/languages/sas": "Sasak",
|
||||||
'/languages/sio': 'Siouan (Other)',
|
"/languages/sio": "Siouan (Other)",
|
||||||
'/languages/sus': 'Susu',
|
"/languages/sus": "Susu",
|
||||||
'/languages/zun': 'Zuni',
|
"/languages/zun": "Zuni",
|
||||||
'/languages/bat': 'Baltic (Other)',
|
"/languages/bat": "Baltic (Other)",
|
||||||
'/languages/car': 'Carib',
|
"/languages/car": "Carib",
|
||||||
'/languages/cha': 'Chamorro',
|
"/languages/cha": "Chamorro",
|
||||||
'/languages/kab': 'Kabyle',
|
"/languages/kab": "Kabyle",
|
||||||
'/languages/kau': 'Kanuri',
|
"/languages/kau": "Kanuri",
|
||||||
'/languages/kho': 'Khotanese',
|
"/languages/kho": "Khotanese",
|
||||||
'/languages/lua': 'Luba-Lulua',
|
"/languages/lua": "Luba-Lulua",
|
||||||
'/languages/mdf': 'Moksha',
|
"/languages/mdf": "Moksha",
|
||||||
'/languages/nbl': 'Ndebele (South Africa)',
|
"/languages/nbl": "Ndebele (South Africa)",
|
||||||
'/languages/umb': 'Umbundu',
|
"/languages/umb": "Umbundu",
|
||||||
'/languages/wak': 'Wakashan languages',
|
"/languages/wak": "Wakashan languages",
|
||||||
'/languages/wal': 'Wolayta',
|
"/languages/wal": "Wolayta",
|
||||||
'/languages/ale': 'Aleut',
|
"/languages/ale": "Aleut",
|
||||||
'/languages/bis': 'Bislama',
|
"/languages/bis": "Bislama",
|
||||||
'/languages/gba': 'Gbaya',
|
"/languages/gba": "Gbaya",
|
||||||
'/languages/glv': 'Manx',
|
"/languages/glv": "Manx",
|
||||||
'/languages/gul': 'Gullah',
|
"/languages/gul": "Gullah",
|
||||||
'/languages/ipk': 'Inupiaq',
|
"/languages/ipk": "Inupiaq",
|
||||||
'/languages/krl': 'Karelian',
|
"/languages/krl": "Karelian",
|
||||||
'/languages/lam': 'Lamba (Zambia and Congo)',
|
"/languages/lam": "Lamba (Zambia and Congo)",
|
||||||
'/languages/sad': 'Sandawe',
|
"/languages/sad": "Sandawe",
|
||||||
'/languages/sid': 'Sidamo',
|
"/languages/sid": "Sidamo",
|
||||||
'/languages/snk': 'Soninke',
|
"/languages/snk": "Soninke",
|
||||||
'/languages/srn': 'Sranan',
|
"/languages/srn": "Sranan",
|
||||||
'/languages/suk': 'Sukuma',
|
"/languages/suk": "Sukuma",
|
||||||
'/languages/ter': 'Terena',
|
"/languages/ter": "Terena",
|
||||||
'/languages/tiv': 'Tiv',
|
"/languages/tiv": "Tiv",
|
||||||
'/languages/tli': 'Tlingit',
|
"/languages/tli": "Tlingit",
|
||||||
'/languages/tpi': 'Tok Pisin',
|
"/languages/tpi": "Tok Pisin",
|
||||||
'/languages/tvl': 'Tuvaluan',
|
"/languages/tvl": "Tuvaluan",
|
||||||
'/languages/yap': 'Yapese',
|
"/languages/yap": "Yapese",
|
||||||
'/languages/eka': 'Ekajuk',
|
"/languages/eka": "Ekajuk",
|
||||||
'/languages/hsb': 'Upper Sorbian',
|
"/languages/hsb": "Upper Sorbian",
|
||||||
'/languages/ido': 'Ido',
|
"/languages/ido": "Ido",
|
||||||
'/languages/kmb': 'Kimbundu',
|
"/languages/kmb": "Kimbundu",
|
||||||
'/languages/kpe': 'Kpelle',
|
"/languages/kpe": "Kpelle",
|
||||||
'/languages/mwl': 'Mirandese',
|
"/languages/mwl": "Mirandese",
|
||||||
'/languages/nno': 'Nynorsk',
|
"/languages/nno": "Nynorsk",
|
||||||
'/languages/nub': 'Nubian languages',
|
"/languages/nub": "Nubian languages",
|
||||||
'/languages/osa': 'Osage',
|
"/languages/osa": "Osage",
|
||||||
'/languages/sme': 'Northern Sami',
|
"/languages/sme": "Northern Sami",
|
||||||
'/languages/znd': 'Zande languages',
|
"/languages/znd": "Zande languages",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' using a bookwyrm instance as a source of book data '''
|
""" using a bookwyrm instance as a source of book data """
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
|
@ -10,10 +10,11 @@ from .abstract_connector import AbstractConnector, SearchResult
|
||||||
|
|
||||||
|
|
||||||
class Connector(AbstractConnector):
|
class Connector(AbstractConnector):
|
||||||
''' instantiate a connector '''
|
""" instantiate a connector """
|
||||||
|
|
||||||
# pylint: disable=arguments-differ
|
# pylint: disable=arguments-differ
|
||||||
def search(self, query, min_confidence=0.1, raw=False):
|
def search(self, query, min_confidence=0.1, raw=False):
|
||||||
''' search your local database '''
|
""" search your local database """
|
||||||
if not query:
|
if not query:
|
||||||
return []
|
return []
|
||||||
# first, try searching unqiue identifiers
|
# first, try searching unqiue identifiers
|
||||||
|
@ -33,19 +34,53 @@ class Connector(AbstractConnector):
|
||||||
search_results.sort(key=lambda r: r.confidence, reverse=True)
|
search_results.sort(key=lambda r: r.confidence, reverse=True)
|
||||||
return search_results
|
return search_results
|
||||||
|
|
||||||
|
def isbn_search(self, query, raw=False):
|
||||||
|
""" search your local database """
|
||||||
|
if not query:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filters = [{f: query} for f in ["isbn_10", "isbn_13"]]
|
||||||
|
results = models.Edition.objects.filter(
|
||||||
|
reduce(operator.or_, (Q(**f) for f in filters))
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# when there are multiple editions of the same work, pick the default.
|
||||||
|
# it would be odd for this to happen.
|
||||||
|
results = results.filter(parent_work__default_edition__id=F("id")) or results
|
||||||
|
|
||||||
|
search_results = []
|
||||||
|
for result in results:
|
||||||
|
if raw:
|
||||||
|
search_results.append(result)
|
||||||
|
else:
|
||||||
|
search_results.append(self.format_search_result(result))
|
||||||
|
if len(search_results) >= 10:
|
||||||
|
break
|
||||||
|
return search_results
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
title=search_result.title,
|
title=search_result.title,
|
||||||
key=search_result.remote_id,
|
key=search_result.remote_id,
|
||||||
author=search_result.author_text,
|
author=search_result.author_text,
|
||||||
year=search_result.published_date.year if \
|
year=search_result.published_date.year
|
||||||
search_result.published_date else None,
|
if search_result.published_date
|
||||||
|
else None,
|
||||||
connector=self,
|
connector=self,
|
||||||
confidence=search_result.rank if \
|
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
|
||||||
hasattr(search_result, 'rank') else 1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
return SearchResult(
|
||||||
|
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,
|
||||||
|
connector=self,
|
||||||
|
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
|
||||||
|
)
|
||||||
|
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
pass
|
pass
|
||||||
|
@ -59,8 +94,12 @@ class Connector(AbstractConnector):
|
||||||
def get_authors_from_data(self, data):
|
def get_authors_from_data(self, data):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
""" it's already in the right format, don't even worry about it """
|
||||||
|
return data
|
||||||
|
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
''' it's already in the right format, don't even worry about it '''
|
""" it's already in the right format, don't even worry about it """
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def expand_book_data(self, book):
|
def expand_book_data(self, book):
|
||||||
|
@ -68,44 +107,47 @@ class Connector(AbstractConnector):
|
||||||
|
|
||||||
|
|
||||||
def search_identifiers(query):
|
def search_identifiers(query):
|
||||||
''' tries remote_id, isbn; defined as dedupe fields on the model '''
|
""" tries remote_id, isbn; defined as dedupe fields on the model """
|
||||||
filters = [{f.name: query} for f in models.Edition._meta.get_fields() \
|
filters = [
|
||||||
if hasattr(f, 'deduplication_field') and f.deduplication_field]
|
{f.name: query}
|
||||||
|
for f in models.Edition._meta.get_fields()
|
||||||
|
if hasattr(f, "deduplication_field") and f.deduplication_field
|
||||||
|
]
|
||||||
results = models.Edition.objects.filter(
|
results = models.Edition.objects.filter(
|
||||||
reduce(operator.or_, (Q(**f) for f in filters))
|
reduce(operator.or_, (Q(**f) for f in filters))
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
# when there are multiple editions of the same work, pick the default.
|
# when there are multiple editions of the same work, pick the default.
|
||||||
# it would be odd for this to happen.
|
# it would be odd for this to happen.
|
||||||
return results.filter(parent_work__default_edition__id=F('id')) \
|
return results.filter(parent_work__default_edition__id=F("id")) or results
|
||||||
or results
|
|
||||||
|
|
||||||
|
|
||||||
def search_title_author(query, min_confidence):
|
def search_title_author(query, min_confidence):
|
||||||
''' searches for title and author '''
|
""" searches for title and author """
|
||||||
vector = SearchVector('title', weight='A') +\
|
vector = (
|
||||||
SearchVector('subtitle', weight='B') +\
|
SearchVector("title", weight="A")
|
||||||
SearchVector('authors__name', weight='C') +\
|
+ SearchVector("subtitle", weight="B")
|
||||||
SearchVector('series', weight='D')
|
+ SearchVector("authors__name", weight="C")
|
||||||
|
+ SearchVector("series", weight="D")
|
||||||
|
)
|
||||||
|
|
||||||
results = models.Edition.objects.annotate(
|
results = (
|
||||||
search=vector
|
models.Edition.objects.annotate(search=vector)
|
||||||
).annotate(
|
.annotate(rank=SearchRank(vector, query))
|
||||||
rank=SearchRank(vector, query)
|
.filter(rank__gt=min_confidence)
|
||||||
).filter(
|
.order_by("-rank")
|
||||||
rank__gt=min_confidence
|
)
|
||||||
).order_by('-rank')
|
|
||||||
|
|
||||||
# when there are multiple editions of the same work, pick the closest
|
# when there are multiple editions of the same work, pick the closest
|
||||||
editions_of_work = results.values(
|
editions_of_work = (
|
||||||
'parent_work'
|
results.values("parent_work")
|
||||||
).annotate(
|
.annotate(Count("parent_work"))
|
||||||
Count('parent_work')
|
.values_list("parent_work")
|
||||||
).values_list('parent_work')
|
)
|
||||||
|
|
||||||
for work_id in set(editions_of_work):
|
for work_id in set(editions_of_work):
|
||||||
editions = results.filter(parent_work=work_id)
|
editions = results.filter(parent_work=work_id)
|
||||||
default = editions.filter(parent_work__default_edition=F('id'))
|
default = editions.filter(parent_work__default_edition=F("id"))
|
||||||
default_rank = default.first().rank if default.exists() else 0
|
default_rank = default.first().rank if default.exists() else 0
|
||||||
# if mutliple books have the top rank, pick the default edition
|
# if mutliple books have the top rank, pick the default edition
|
||||||
if default_rank == editions.first().rank:
|
if default_rank == editions.first().rank:
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
''' settings book data connectors '''
|
""" settings book data connectors """
|
||||||
|
|
||||||
CONNECTORS = ['openlibrary', 'self_connector', 'bookwyrm_connector']
|
CONNECTORS = ["openlibrary", "self_connector", "bookwyrm_connector"]
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
''' customize the info available in context for rendering templates '''
|
""" customize the info available in context for rendering templates """
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
||||||
def site_settings(request): # pylint: disable=unused-argument
|
def site_settings(request): # pylint: disable=unused-argument
|
||||||
''' include the custom info about the site '''
|
""" include the custom info about the site """
|
||||||
return {
|
return {"site": models.SiteSettings.objects.get()}
|
||||||
'site': models.SiteSettings.objects.get()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
''' send emails '''
|
""" send emails """
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
|
||||||
def password_reset_email(reset_code):
|
def password_reset_email(reset_code):
|
||||||
''' generate a password reset email '''
|
""" generate a password reset email """
|
||||||
site = models.SiteSettings.get()
|
site = models.SiteSettings.get()
|
||||||
send_email.delay(
|
send_email.delay(
|
||||||
reset_code.user.email,
|
reset_code.user.email,
|
||||||
'Reset your password on %s' % site.name,
|
"Reset your password on %s" % site.name,
|
||||||
'Your password reset link: %s' % reset_code.link
|
"Your password reset link: %s" % reset_code.link,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def send_email(recipient, subject, message):
|
def send_email(recipient, subject, message):
|
||||||
''' use a task to send the email '''
|
""" use a task to send the email """
|
||||||
send_mail(
|
send_mail(
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
None, # sender will be the config default
|
None, # sender will be the config default
|
||||||
[recipient],
|
[recipient],
|
||||||
fail_silently=False
|
fail_silently=False,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' using django model forms '''
|
""" using django model forms """
|
||||||
import datetime
|
import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
@ -6,120 +6,131 @@ from django import forms
|
||||||
from django.forms import ModelForm, PasswordInput, widgets
|
from django.forms import ModelForm, PasswordInput, widgets
|
||||||
from django.forms.widgets import Textarea
|
from django.forms.widgets import Textarea
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
||||||
class CustomForm(ModelForm):
|
class CustomForm(ModelForm):
|
||||||
''' add css classes to the forms '''
|
""" add css classes to the forms """
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
css_classes = defaultdict(lambda: '')
|
css_classes = defaultdict(lambda: "")
|
||||||
css_classes['text'] = 'input'
|
css_classes["text"] = "input"
|
||||||
css_classes['password'] = 'input'
|
css_classes["password"] = "input"
|
||||||
css_classes['email'] = 'input'
|
css_classes["email"] = "input"
|
||||||
css_classes['number'] = 'input'
|
css_classes["number"] = "input"
|
||||||
css_classes['checkbox'] = 'checkbox'
|
css_classes["checkbox"] = "checkbox"
|
||||||
css_classes['textarea'] = 'textarea'
|
css_classes["textarea"] = "textarea"
|
||||||
super(CustomForm, self).__init__(*args, **kwargs)
|
super(CustomForm, self).__init__(*args, **kwargs)
|
||||||
for visible in self.visible_fields():
|
for visible in self.visible_fields():
|
||||||
if hasattr(visible.field.widget, 'input_type'):
|
if hasattr(visible.field.widget, "input_type"):
|
||||||
input_type = visible.field.widget.input_type
|
input_type = visible.field.widget.input_type
|
||||||
if isinstance(visible.field.widget, Textarea):
|
if isinstance(visible.field.widget, Textarea):
|
||||||
input_type = 'textarea'
|
input_type = "textarea"
|
||||||
visible.field.widget.attrs['cols'] = None
|
visible.field.widget.attrs["cols"] = None
|
||||||
visible.field.widget.attrs['rows'] = None
|
visible.field.widget.attrs["rows"] = None
|
||||||
visible.field.widget.attrs['class'] = css_classes[input_type]
|
visible.field.widget.attrs["class"] = css_classes[input_type]
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
# pylint: disable=missing-class-docstring
|
||||||
class LoginForm(CustomForm):
|
class LoginForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
fields = ['localname', 'password']
|
fields = ["localname", "password"]
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
widgets = {
|
widgets = {
|
||||||
'password': PasswordInput(),
|
"password": PasswordInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RegisterForm(CustomForm):
|
class RegisterForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
fields = ['localname', 'email', 'password']
|
fields = ["localname", "email", "password"]
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
widgets = {
|
widgets = {"password": PasswordInput()}
|
||||||
'password': PasswordInput()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RatingForm(CustomForm):
|
class RatingForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ReviewRating
|
model = models.ReviewRating
|
||||||
fields = ['user', 'book', 'rating', 'privacy']
|
fields = ["user", "book", "rating", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
class ReviewForm(CustomForm):
|
class ReviewForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Review
|
model = models.Review
|
||||||
fields = [
|
fields = [
|
||||||
'user', 'book',
|
"user",
|
||||||
'name', 'content', 'rating',
|
"book",
|
||||||
'content_warning', 'sensitive',
|
"name",
|
||||||
'privacy']
|
"content",
|
||||||
|
"rating",
|
||||||
|
"content_warning",
|
||||||
|
"sensitive",
|
||||||
|
"privacy",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(CustomForm):
|
class CommentForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Comment
|
model = models.Comment
|
||||||
fields = [
|
fields = ["user", "book", "content", "content_warning", "sensitive", "privacy"]
|
||||||
'user', 'book', 'content',
|
|
||||||
'content_warning', 'sensitive',
|
|
||||||
'privacy']
|
|
||||||
|
|
||||||
|
|
||||||
class QuotationForm(CustomForm):
|
class QuotationForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Quotation
|
model = models.Quotation
|
||||||
fields = [
|
fields = [
|
||||||
'user', 'book', 'quote', 'content',
|
"user",
|
||||||
'content_warning', 'sensitive', 'privacy']
|
"book",
|
||||||
|
"quote",
|
||||||
|
"content",
|
||||||
|
"content_warning",
|
||||||
|
"sensitive",
|
||||||
|
"privacy",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ReplyForm(CustomForm):
|
class ReplyForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Status
|
model = models.Status
|
||||||
fields = [
|
fields = [
|
||||||
'user', 'content', 'content_warning', 'sensitive',
|
"user",
|
||||||
'reply_parent', 'privacy']
|
"content",
|
||||||
|
"content_warning",
|
||||||
|
"sensitive",
|
||||||
|
"reply_parent",
|
||||||
|
"privacy",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class StatusForm(CustomForm):
|
class StatusForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Status
|
model = models.Status
|
||||||
fields = [
|
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||||
'user', 'content', 'content_warning', 'sensitive', 'privacy']
|
|
||||||
|
|
||||||
|
|
||||||
class EditUserForm(CustomForm):
|
class EditUserForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
fields = [
|
fields = ["avatar", "name", "email", "summary", "manually_approves_followers"]
|
||||||
'avatar', 'name', 'email', 'summary', 'manually_approves_followers'
|
|
||||||
]
|
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
||||||
class TagForm(CustomForm):
|
class TagForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Tag
|
model = models.Tag
|
||||||
fields = ['name']
|
fields = ["name"]
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
labels = {'name': 'Add a tag'}
|
labels = {"name": "Add a tag"}
|
||||||
|
|
||||||
|
|
||||||
class CoverForm(CustomForm):
|
class CoverForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Book
|
model = models.Book
|
||||||
fields = ['cover']
|
fields = ["cover"]
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
||||||
|
@ -127,79 +138,87 @@ class EditionForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Edition
|
model = models.Edition
|
||||||
exclude = [
|
exclude = [
|
||||||
'remote_id',
|
"remote_id",
|
||||||
'origin_id',
|
"origin_id",
|
||||||
'created_date',
|
"created_date",
|
||||||
'updated_date',
|
"updated_date",
|
||||||
'edition_rank',
|
"edition_rank",
|
||||||
|
"authors", # TODO
|
||||||
'authors',# TODO
|
"parent_work",
|
||||||
'parent_work',
|
"shelves",
|
||||||
'shelves',
|
"subjects", # TODO
|
||||||
|
"subject_places", # TODO
|
||||||
'subjects',# TODO
|
"connector",
|
||||||
'subject_places',# TODO
|
|
||||||
|
|
||||||
'connector',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AuthorForm(CustomForm):
|
class AuthorForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Author
|
model = models.Author
|
||||||
exclude = [
|
exclude = [
|
||||||
'remote_id',
|
"remote_id",
|
||||||
'origin_id',
|
"origin_id",
|
||||||
'created_date',
|
"created_date",
|
||||||
'updated_date',
|
"updated_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ImportForm(forms.Form):
|
class ImportForm(forms.Form):
|
||||||
csv_file = forms.FileField()
|
csv_file = forms.FileField()
|
||||||
|
|
||||||
|
|
||||||
class ExpiryWidget(widgets.Select):
|
class ExpiryWidget(widgets.Select):
|
||||||
def value_from_datadict(self, data, files, name):
|
def value_from_datadict(self, data, files, name):
|
||||||
''' human-readable exiration time buckets '''
|
""" human-readable exiration time buckets """
|
||||||
selected_string = super().value_from_datadict(data, files, name)
|
selected_string = super().value_from_datadict(data, files, name)
|
||||||
|
|
||||||
if selected_string == 'day':
|
if selected_string == "day":
|
||||||
interval = datetime.timedelta(days=1)
|
interval = datetime.timedelta(days=1)
|
||||||
elif selected_string == 'week':
|
elif selected_string == "week":
|
||||||
interval = datetime.timedelta(days=7)
|
interval = datetime.timedelta(days=7)
|
||||||
elif selected_string == 'month':
|
elif selected_string == "month":
|
||||||
interval = datetime.timedelta(days=31) # Close enough?
|
interval = datetime.timedelta(days=31) # Close enough?
|
||||||
elif selected_string == 'forever':
|
elif selected_string == "forever":
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return selected_string # "This will raise
|
return selected_string # "This will raise
|
||||||
|
|
||||||
return timezone.now() + interval
|
return timezone.now() + interval
|
||||||
|
|
||||||
|
|
||||||
class CreateInviteForm(CustomForm):
|
class CreateInviteForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.SiteInvite
|
model = models.SiteInvite
|
||||||
exclude = ['code', 'user', 'times_used']
|
exclude = ["code", "user", "times_used"]
|
||||||
widgets = {
|
widgets = {
|
||||||
'expiry': ExpiryWidget(choices=[
|
"expiry": ExpiryWidget(
|
||||||
('day', 'One Day'),
|
choices=[
|
||||||
('week', 'One Week'),
|
("day", _("One Day")),
|
||||||
('month', 'One Month'),
|
("week", _("One Week")),
|
||||||
('forever', 'Does Not Expire')]),
|
("month", _("One Month")),
|
||||||
'use_limit': widgets.Select(
|
("forever", _("Does Not Expire")),
|
||||||
choices=[(i, "%d uses" % (i,)) for i in [1, 5, 10, 25, 50, 100]]
|
]
|
||||||
+ [(None, 'Unlimited')])
|
),
|
||||||
|
"use_limit": widgets.Select(
|
||||||
|
choices=[
|
||||||
|
(i, _("%(count)d uses" % {"count": i}))
|
||||||
|
for i in [1, 5, 10, 25, 50, 100]
|
||||||
|
]
|
||||||
|
+ [(None, _("Unlimited"))]
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ShelfForm(CustomForm):
|
class ShelfForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Shelf
|
model = models.Shelf
|
||||||
fields = ['user', 'name', 'privacy']
|
fields = ["user", "name", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
class GoalForm(CustomForm):
|
class GoalForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.AnnualGoal
|
model = models.AnnualGoal
|
||||||
fields = ['user', 'year', 'goal', 'privacy']
|
fields = ["user", "year", "goal", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
class SiteForm(CustomForm):
|
class SiteForm(CustomForm):
|
||||||
|
@ -211,4 +230,4 @@ class SiteForm(CustomForm):
|
||||||
class ListForm(CustomForm):
|
class ListForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.List
|
model = models.List
|
||||||
fields = ['user', 'name', 'description', 'curation', 'privacy']
|
fields = ["user", "name", "description", "curation", "privacy"]
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
''' handle reading a csv from goodreads '''
|
""" handle reading a csv from goodreads """
|
||||||
from bookwyrm.importer import Importer
|
from bookwyrm.importer import Importer
|
||||||
|
|
||||||
# GoodReads is the default importer, thus Importer follows its structure. For a more complete example of overriding see librarything_import.py
|
# GoodReads is the default importer, thus Importer follows its structure. For a more complete example of overriding see librarything_import.py
|
||||||
|
|
||||||
|
|
||||||
class GoodreadsImporter(Importer):
|
class GoodreadsImporter(Importer):
|
||||||
service = 'GoodReads'
|
service = "GoodReads"
|
||||||
|
|
||||||
def parse_fields(self, data):
|
def parse_fields(self, data):
|
||||||
data.update({'import_source': self.service })
|
data.update({"import_source": self.service})
|
||||||
# add missing 'Date Started' field
|
# add missing 'Date Started' field
|
||||||
data.update({'Date Started': None })
|
data.update({"Date Started": None})
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' handle reading a csv from an external service, defaults are from GoodReads '''
|
""" handle reading a csv from an external service, defaults are from GoodReads """
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -8,49 +8,48 @@ from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Importer:
|
class Importer:
|
||||||
service = 'Unknown'
|
service = "Unknown"
|
||||||
delimiter = ','
|
delimiter = ","
|
||||||
encoding = 'UTF-8'
|
encoding = "UTF-8"
|
||||||
mandatory_fields = ['Title', 'Author']
|
mandatory_fields = ["Title", "Author"]
|
||||||
|
|
||||||
def create_job(self, user, csv_file, include_reviews, privacy):
|
def create_job(self, user, csv_file, include_reviews, privacy):
|
||||||
''' check over a csv and creates a database entry for the job'''
|
""" check over a csv and creates a database entry for the job"""
|
||||||
job = ImportJob.objects.create(
|
job = ImportJob.objects.create(
|
||||||
user=user,
|
user=user, include_reviews=include_reviews, privacy=privacy
|
||||||
include_reviews=include_reviews,
|
|
||||||
privacy=privacy
|
|
||||||
)
|
)
|
||||||
for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.delimiter ))):
|
for index, entry in enumerate(
|
||||||
|
list(csv.DictReader(csv_file, delimiter=self.delimiter))
|
||||||
|
):
|
||||||
if not all(x in entry for x in self.mandatory_fields):
|
if not all(x in entry for x in self.mandatory_fields):
|
||||||
raise ValueError('Author and title must be in data.')
|
raise ValueError("Author and title must be in data.")
|
||||||
entry = self.parse_fields(entry)
|
entry = self.parse_fields(entry)
|
||||||
self.save_item(job, index, entry)
|
self.save_item(job, index, entry)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
def save_item(self, job, index, data):
|
def save_item(self, job, index, data):
|
||||||
ImportItem(job=job, index=index, data=data).save()
|
ImportItem(job=job, index=index, data=data).save()
|
||||||
|
|
||||||
def parse_fields(self, entry):
|
def parse_fields(self, entry):
|
||||||
entry.update({'import_source': self.service })
|
entry.update({"import_source": self.service})
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
def create_retry_job(self, user, original_job, items):
|
def create_retry_job(self, user, original_job, items):
|
||||||
''' retry items that didn't import '''
|
""" retry items that didn't import """
|
||||||
job = ImportJob.objects.create(
|
job = ImportJob.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
include_reviews=original_job.include_reviews,
|
include_reviews=original_job.include_reviews,
|
||||||
privacy=original_job.privacy,
|
privacy=original_job.privacy,
|
||||||
retry=True
|
retry=True,
|
||||||
)
|
)
|
||||||
for item in items:
|
for item in items:
|
||||||
self.save_item(job, item.index, item.data)
|
self.save_item(job, item.index, item.data)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
def start_import(self, job):
|
def start_import(self, job):
|
||||||
''' initalizes a csv import job '''
|
""" initalizes a csv import job """
|
||||||
result = import_data.delay(self.service, job.id)
|
result = import_data.delay(self.service, job.id)
|
||||||
job.task_id = result.id
|
job.task_id = result.id
|
||||||
job.save()
|
job.save()
|
||||||
|
@ -58,7 +57,7 @@ class Importer:
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def import_data(source, job_id):
|
def import_data(source, job_id):
|
||||||
''' does the actual lookup work in a celery task '''
|
""" does the actual lookup work in a celery task """
|
||||||
job = ImportJob.objects.get(id=job_id)
|
job = ImportJob.objects.get(id=job_id)
|
||||||
try:
|
try:
|
||||||
for item in job.items.all():
|
for item in job.items.all():
|
||||||
|
@ -66,7 +65,7 @@ def import_data(source, job_id):
|
||||||
item.resolve()
|
item.resolve()
|
||||||
except Exception as e: # pylint: disable=broad-except
|
except Exception as e: # pylint: disable=broad-except
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
item.fail_reason = 'Error loading book'
|
item.fail_reason = "Error loading book"
|
||||||
item.save()
|
item.save()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -74,10 +73,11 @@ def import_data(source, job_id):
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
# shelves book and handles reviews
|
# shelves book and handles reviews
|
||||||
handle_imported_book(source,
|
handle_imported_book(
|
||||||
job.user, item, job.include_reviews, job.privacy)
|
source, job.user, item, job.include_reviews, job.privacy
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
item.fail_reason = 'Could not find a match for book'
|
item.fail_reason = "Could not find a match for book"
|
||||||
item.save()
|
item.save()
|
||||||
finally:
|
finally:
|
||||||
job.complete = True
|
job.complete = True
|
||||||
|
@ -85,30 +85,26 @@ def import_data(source, job_id):
|
||||||
|
|
||||||
|
|
||||||
def handle_imported_book(source, user, item, include_reviews, privacy):
|
def handle_imported_book(source, user, item, include_reviews, privacy):
|
||||||
''' process a csv and then post about it '''
|
""" process a csv and then post about it """
|
||||||
if isinstance(item.book, models.Work):
|
if isinstance(item.book, models.Work):
|
||||||
item.book = item.book.default_edition
|
item.book = item.book.default_edition
|
||||||
if not item.book:
|
if not item.book:
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_shelf = models.ShelfBook.objects.filter(
|
existing_shelf = models.ShelfBook.objects.filter(book=item.book, user=user).exists()
|
||||||
book=item.book, user=user).exists()
|
|
||||||
|
|
||||||
# shelve the book if it hasn't been shelved already
|
# shelve the book if it hasn't been shelved already
|
||||||
if item.shelf and not existing_shelf:
|
if item.shelf and not existing_shelf:
|
||||||
desired_shelf = models.Shelf.objects.get(
|
desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user)
|
||||||
identifier=item.shelf,
|
models.ShelfBook.objects.create(book=item.book, shelf=desired_shelf, user=user)
|
||||||
user=user
|
|
||||||
)
|
|
||||||
models.ShelfBook.objects.create(
|
|
||||||
book=item.book, shelf=desired_shelf, user=user)
|
|
||||||
|
|
||||||
for read in item.reads:
|
for read in item.reads:
|
||||||
# check for an existing readthrough with the same dates
|
# check for an existing readthrough with the same dates
|
||||||
if models.ReadThrough.objects.filter(
|
if models.ReadThrough.objects.filter(
|
||||||
user=user, book=item.book,
|
user=user,
|
||||||
|
book=item.book,
|
||||||
start_date=read.start_date,
|
start_date=read.start_date,
|
||||||
finish_date=read.finish_date
|
finish_date=read.finish_date,
|
||||||
).exists():
|
).exists():
|
||||||
continue
|
continue
|
||||||
read.book = item.book
|
read.book = item.book
|
||||||
|
@ -116,10 +112,14 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
|
||||||
read.save()
|
read.save()
|
||||||
|
|
||||||
if include_reviews and (item.rating or item.review):
|
if include_reviews and (item.rating or item.review):
|
||||||
review_title = 'Review of {!r} on {!r}'.format(
|
review_title = (
|
||||||
|
"Review of {!r} on {!r}".format(
|
||||||
item.book.title,
|
item.book.title,
|
||||||
source,
|
source,
|
||||||
) if item.review else ''
|
)
|
||||||
|
if item.review
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
# we don't know the publication date of the review,
|
# we don't know the publication date of the review,
|
||||||
# but "now" is a bad guess
|
# but "now" is a bad guess
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' handle reading a csv from librarything '''
|
""" handle reading a csv from librarything """
|
||||||
import csv
|
import csv
|
||||||
import re
|
import re
|
||||||
import math
|
import math
|
||||||
|
@ -9,34 +9,34 @@ from bookwyrm.importer import Importer
|
||||||
|
|
||||||
|
|
||||||
class LibrarythingImporter(Importer):
|
class LibrarythingImporter(Importer):
|
||||||
service = 'LibraryThing'
|
service = "LibraryThing"
|
||||||
delimiter = '\t'
|
delimiter = "\t"
|
||||||
encoding = 'ISO-8859-1'
|
encoding = "ISO-8859-1"
|
||||||
# mandatory_fields : fields matching the book title and author
|
# mandatory_fields : fields matching the book title and author
|
||||||
mandatory_fields = ['Title', 'Primary Author']
|
mandatory_fields = ["Title", "Primary Author"]
|
||||||
|
|
||||||
def parse_fields(self, initial):
|
def parse_fields(self, initial):
|
||||||
data = {}
|
data = {}
|
||||||
data['import_source'] = self.service
|
data["import_source"] = self.service
|
||||||
data['Book Id'] = initial['Book Id']
|
data["Book Id"] = initial["Book Id"]
|
||||||
data['Title'] = initial['Title']
|
data["Title"] = initial["Title"]
|
||||||
data['Author'] = initial['Primary Author']
|
data["Author"] = initial["Primary Author"]
|
||||||
data['ISBN13'] = initial['ISBN']
|
data["ISBN13"] = initial["ISBN"]
|
||||||
data['My Review'] = initial['Review']
|
data["My Review"] = initial["Review"]
|
||||||
if initial['Rating']:
|
if initial["Rating"]:
|
||||||
data['My Rating'] = math.ceil(float(initial['Rating']))
|
data["My Rating"] = math.ceil(float(initial["Rating"]))
|
||||||
else:
|
else:
|
||||||
data['My Rating'] = ''
|
data["My Rating"] = ""
|
||||||
data['Date Added'] = re.sub('\[|\]', '', initial['Entry Date'])
|
data["Date Added"] = re.sub("\[|\]", "", initial["Entry Date"])
|
||||||
data['Date Started'] = re.sub('\[|\]', '', initial['Date Started'])
|
data["Date Started"] = re.sub("\[|\]", "", initial["Date Started"])
|
||||||
data['Date Read'] = re.sub('\[|\]', '', initial['Date Read'])
|
data["Date Read"] = re.sub("\[|\]", "", initial["Date Read"])
|
||||||
|
|
||||||
data['Exclusive Shelf'] = None
|
data["Exclusive Shelf"] = None
|
||||||
if data['Date Read']:
|
if data["Date Read"]:
|
||||||
data['Exclusive Shelf'] = "read"
|
data["Exclusive Shelf"] = "read"
|
||||||
elif data['Date Started']:
|
elif data["Date Started"]:
|
||||||
data['Exclusive Shelf'] = "reading"
|
data["Exclusive Shelf"] = "reading"
|
||||||
else:
|
else:
|
||||||
data['Exclusive Shelf'] = "to-read"
|
data["Exclusive Shelf"] = "to-read"
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -1,26 +1,20 @@
|
||||||
''' PROCEED WITH CAUTION: uses deduplication fields to permanently
|
""" PROCEED WITH CAUTION: uses deduplication fields to permanently
|
||||||
merge book data objects '''
|
merge book data objects """
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
||||||
def update_related(canonical, obj):
|
def update_related(canonical, obj):
|
||||||
''' update all the models with fk to the object being removed '''
|
""" update all the models with fk to the object being removed """
|
||||||
# move related models to canonical
|
# move related models to canonical
|
||||||
related_models = [
|
related_models = [
|
||||||
(r.remote_field.name, r.related_model) for r in \
|
(r.remote_field.name, r.related_model) for r in canonical._meta.related_objects
|
||||||
canonical._meta.related_objects]
|
]
|
||||||
for (related_field, related_model) in related_models:
|
for (related_field, related_model) in related_models:
|
||||||
related_objs = related_model.objects.filter(
|
related_objs = related_model.objects.filter(**{related_field: obj})
|
||||||
**{related_field: obj})
|
|
||||||
for related_obj in related_objs:
|
for related_obj in related_objs:
|
||||||
print(
|
print("replacing in", related_model.__name__, related_field, related_obj.id)
|
||||||
'replacing in',
|
|
||||||
related_model.__name__,
|
|
||||||
related_field,
|
|
||||||
related_obj.id
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
setattr(related_obj, related_field, canonical)
|
setattr(related_obj, related_field, canonical)
|
||||||
related_obj.save()
|
related_obj.save()
|
||||||
|
@ -30,40 +24,41 @@ def update_related(canonical, obj):
|
||||||
|
|
||||||
|
|
||||||
def copy_data(canonical, obj):
|
def copy_data(canonical, obj):
|
||||||
''' try to get the most data possible '''
|
""" try to get the most data possible """
|
||||||
for data_field in obj._meta.get_fields():
|
for data_field in obj._meta.get_fields():
|
||||||
if not hasattr(data_field, 'activitypub_field'):
|
if not hasattr(data_field, "activitypub_field"):
|
||||||
continue
|
continue
|
||||||
data_value = getattr(obj, data_field.name)
|
data_value = getattr(obj, data_field.name)
|
||||||
if not data_value:
|
if not data_value:
|
||||||
continue
|
continue
|
||||||
if not getattr(canonical, data_field.name):
|
if not getattr(canonical, data_field.name):
|
||||||
print('setting data field', data_field.name, data_value)
|
print("setting data field", data_field.name, data_value)
|
||||||
setattr(canonical, data_field.name, data_value)
|
setattr(canonical, data_field.name, data_value)
|
||||||
canonical.save()
|
canonical.save()
|
||||||
|
|
||||||
|
|
||||||
def dedupe_model(model):
|
def dedupe_model(model):
|
||||||
''' combine duplicate editions and update related models '''
|
""" combine duplicate editions and update related models """
|
||||||
fields = model._meta.get_fields()
|
fields = model._meta.get_fields()
|
||||||
dedupe_fields = [f for f in fields if \
|
dedupe_fields = [
|
||||||
hasattr(f, 'deduplication_field') and f.deduplication_field]
|
f for f in fields if hasattr(f, "deduplication_field") and f.deduplication_field
|
||||||
|
]
|
||||||
for field in dedupe_fields:
|
for field in dedupe_fields:
|
||||||
dupes = model.objects.values(field.name).annotate(
|
dupes = (
|
||||||
Count(field.name)
|
model.objects.values(field.name)
|
||||||
).filter(**{'%s__count__gt' % field.name: 1})
|
.annotate(Count(field.name))
|
||||||
|
.filter(**{"%s__count__gt" % field.name: 1})
|
||||||
|
)
|
||||||
|
|
||||||
for dupe in dupes:
|
for dupe in dupes:
|
||||||
value = dupe[field.name]
|
value = dupe[field.name]
|
||||||
if not value or value == '':
|
if not value or value == "":
|
||||||
continue
|
continue
|
||||||
print('----------')
|
print("----------")
|
||||||
print(dupe)
|
print(dupe)
|
||||||
objs = model.objects.filter(
|
objs = model.objects.filter(**{field.name: value}).order_by("id")
|
||||||
**{field.name: value}
|
|
||||||
).order_by('id')
|
|
||||||
canonical = objs.first()
|
canonical = objs.first()
|
||||||
print('keeping', canonical.remote_id)
|
print("keeping", canonical.remote_id)
|
||||||
for obj in objs[1:]:
|
for obj in objs[1:]:
|
||||||
print(obj.remote_id)
|
print(obj.remote_id)
|
||||||
copy_data(canonical, obj)
|
copy_data(canonical, obj)
|
||||||
|
@ -73,11 +68,12 @@ def dedupe_model(model):
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
''' dedplucate allllll the book data models '''
|
""" dedplucate allllll the book data models """
|
||||||
help = 'merges duplicate book data'
|
|
||||||
|
help = "merges duplicate book data"
|
||||||
# pylint: disable=no-self-use,unused-argument
|
# pylint: disable=no-self-use,unused-argument
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
''' run deudplications '''
|
""" run deudplications """
|
||||||
dedupe_model(models.Edition)
|
dedupe_model(models.Edition)
|
||||||
dedupe_model(models.Work)
|
dedupe_model(models.Work)
|
||||||
dedupe_model(models.Author)
|
dedupe_model(models.Author)
|
||||||
|
|
|
@ -5,51 +5,63 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
from bookwyrm.models import Connector, SiteSettings, User
|
from bookwyrm.models import Connector, SiteSettings, User
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
def init_groups():
|
def init_groups():
|
||||||
groups = ['admin', 'moderator', 'editor']
|
groups = ["admin", "moderator", "editor"]
|
||||||
for group in groups:
|
for group in groups:
|
||||||
Group.objects.create(name=group)
|
Group.objects.create(name=group)
|
||||||
|
|
||||||
|
|
||||||
def init_permissions():
|
def init_permissions():
|
||||||
permissions = [{
|
permissions = [
|
||||||
'codename': 'edit_instance_settings',
|
{
|
||||||
'name': 'change the instance info',
|
"codename": "edit_instance_settings",
|
||||||
'groups': ['admin',]
|
"name": "change the instance info",
|
||||||
}, {
|
"groups": [
|
||||||
'codename': 'set_user_group',
|
"admin",
|
||||||
'name': 'change what group a user is in',
|
],
|
||||||
'groups': ['admin', 'moderator']
|
},
|
||||||
}, {
|
{
|
||||||
'codename': 'control_federation',
|
"codename": "set_user_group",
|
||||||
'name': 'control who to federate with',
|
"name": "change what group a user is in",
|
||||||
'groups': ['admin', 'moderator']
|
"groups": ["admin", "moderator"],
|
||||||
}, {
|
},
|
||||||
'codename': 'create_invites',
|
{
|
||||||
'name': 'issue invitations to join',
|
"codename": "control_federation",
|
||||||
'groups': ['admin', 'moderator']
|
"name": "control who to federate with",
|
||||||
}, {
|
"groups": ["admin", "moderator"],
|
||||||
'codename': 'moderate_user',
|
},
|
||||||
'name': 'deactivate or silence a user',
|
{
|
||||||
'groups': ['admin', 'moderator']
|
"codename": "create_invites",
|
||||||
}, {
|
"name": "issue invitations to join",
|
||||||
'codename': 'moderate_post',
|
"groups": ["admin", "moderator"],
|
||||||
'name': 'delete other users\' posts',
|
},
|
||||||
'groups': ['admin', 'moderator']
|
{
|
||||||
}, {
|
"codename": "moderate_user",
|
||||||
'codename': 'edit_book',
|
"name": "deactivate or silence a user",
|
||||||
'name': 'edit book info',
|
"groups": ["admin", "moderator"],
|
||||||
'groups': ['admin', 'moderator', 'editor']
|
},
|
||||||
}]
|
{
|
||||||
|
"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)
|
content_type = ContentType.objects.get_for_model(User)
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
permission_obj = Permission.objects.create(
|
permission_obj = Permission.objects.create(
|
||||||
codename=permission['codename'],
|
codename=permission["codename"],
|
||||||
name=permission['name'],
|
name=permission["name"],
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
)
|
)
|
||||||
# add the permission to the appropriate groups
|
# add the permission to the appropriate groups
|
||||||
for group_name in permission['groups']:
|
for group_name in permission["groups"]:
|
||||||
Group.objects.get(name=group_name).permissions.add(permission_obj)
|
Group.objects.get(name=group_name).permissions.add(permission_obj)
|
||||||
|
|
||||||
# while the groups and permissions shouldn't be changed because the code
|
# while the groups and permissions shouldn't be changed because the code
|
||||||
|
@ -59,43 +71,48 @@ def init_permissions():
|
||||||
def init_connectors():
|
def init_connectors():
|
||||||
Connector.objects.create(
|
Connector.objects.create(
|
||||||
identifier=DOMAIN,
|
identifier=DOMAIN,
|
||||||
name='Local',
|
name="Local",
|
||||||
local=True,
|
local=True,
|
||||||
connector_file='self_connector',
|
connector_file="self_connector",
|
||||||
base_url='https://%s' % DOMAIN,
|
base_url="https://%s" % DOMAIN,
|
||||||
books_url='https://%s/book' % DOMAIN,
|
books_url="https://%s/book" % DOMAIN,
|
||||||
covers_url='https://%s/images/covers' % DOMAIN,
|
covers_url="https://%s/images/covers" % DOMAIN,
|
||||||
search_url='https://%s/search?q=' % DOMAIN,
|
search_url="https://%s/search?q=" % DOMAIN,
|
||||||
|
isbn_search_url="https://%s/isbn/" % DOMAIN,
|
||||||
priority=1,
|
priority=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
Connector.objects.create(
|
Connector.objects.create(
|
||||||
identifier='bookwyrm.social',
|
identifier="bookwyrm.social",
|
||||||
name='BookWyrm dot Social',
|
name="BookWyrm dot Social",
|
||||||
connector_file='bookwyrm_connector',
|
connector_file="bookwyrm_connector",
|
||||||
base_url='https://bookwyrm.social',
|
base_url="https://bookwyrm.social",
|
||||||
books_url='https://bookwyrm.social/book',
|
books_url="https://bookwyrm.social/book",
|
||||||
covers_url='https://bookwyrm.social/images/covers',
|
covers_url="https://bookwyrm.social/images/covers",
|
||||||
search_url='https://bookwyrm.social/search?q=',
|
search_url="https://bookwyrm.social/search?q=",
|
||||||
|
isbn_search_url="https://bookwyrm.social/isbn/",
|
||||||
priority=2,
|
priority=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
Connector.objects.create(
|
Connector.objects.create(
|
||||||
identifier='openlibrary.org',
|
identifier="openlibrary.org",
|
||||||
name='OpenLibrary',
|
name="OpenLibrary",
|
||||||
connector_file='openlibrary',
|
connector_file="openlibrary",
|
||||||
base_url='https://openlibrary.org',
|
base_url="https://openlibrary.org",
|
||||||
books_url='https://openlibrary.org',
|
books_url="https://openlibrary.org",
|
||||||
covers_url='https://covers.openlibrary.org',
|
covers_url="https://covers.openlibrary.org",
|
||||||
search_url='https://openlibrary.org/search?q=',
|
search_url="https://openlibrary.org/search?q=",
|
||||||
|
isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:",
|
||||||
priority=3,
|
priority=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init_settings():
|
def init_settings():
|
||||||
SiteSettings.objects.create()
|
SiteSettings.objects.create()
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Initializes the database with starter data'
|
help = "Initializes the database with starter data"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
init_groups()
|
init_groups()
|
||||||
|
|
|
@ -1,34 +1,42 @@
|
||||||
''' PROCEED WITH CAUTION: this permanently deletes book data '''
|
""" PROCEED WITH CAUTION: this permanently deletes book data """
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
||||||
def remove_editions():
|
def remove_editions():
|
||||||
''' combine duplicate editions and update related models '''
|
""" combine duplicate editions and update related models """
|
||||||
# not in use
|
# not in use
|
||||||
filters = {'%s__isnull' % r.name: True \
|
filters = {
|
||||||
for r in models.Edition._meta.related_objects}
|
"%s__isnull" % r.name: True for r in models.Edition._meta.related_objects
|
||||||
|
}
|
||||||
# no cover, no identifying fields
|
# no cover, no identifying fields
|
||||||
filters['cover'] = ''
|
filters["cover"] = ""
|
||||||
null_fields = {'%s__isnull' % f: True for f in \
|
null_fields = {
|
||||||
['isbn_10', 'isbn_13', 'oclc_number']}
|
"%s__isnull" % f: True for f in ["isbn_10", "isbn_13", "oclc_number"]
|
||||||
|
}
|
||||||
|
|
||||||
editions = models.Edition.objects.filter(
|
editions = (
|
||||||
Q(languages=[]) | Q(languages__contains=['English']),
|
models.Edition.objects.filter(
|
||||||
**filters, **null_fields
|
Q(languages=[]) | Q(languages__contains=["English"]),
|
||||||
).annotate(Count('parent_work__editions')).filter(
|
**filters,
|
||||||
|
**null_fields
|
||||||
|
)
|
||||||
|
.annotate(Count("parent_work__editions"))
|
||||||
|
.filter(
|
||||||
# mustn't be the only edition for the work
|
# mustn't be the only edition for the work
|
||||||
parent_work__editions__count__gt=1
|
parent_work__editions__count__gt=1
|
||||||
)
|
)
|
||||||
|
)
|
||||||
print(editions.count())
|
print(editions.count())
|
||||||
editions.delete()
|
editions.delete()
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
''' dedplucate allllll the book data models '''
|
""" dedplucate allllll the book data models """
|
||||||
help = 'merges duplicate book data'
|
|
||||||
|
help = "merges duplicate book data"
|
||||||
# pylint: disable=no-self-use,unused-argument
|
# pylint: disable=no-self-use,unused-argument
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
''' run deudplications '''
|
""" run deudplications """
|
||||||
remove_editions()
|
remove_editions()
|
||||||
|
|
|
@ -15,199 +15,448 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('auth', '0011_update_proxy_permissions'),
|
("auth", "0011_update_proxy_permissions"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='User',
|
name="User",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
"id",
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
models.AutoField(
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
auto_created=True,
|
||||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
primary_key=True,
|
||||||
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
|
serialize=False,
|
||||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
verbose_name="ID",
|
||||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
),
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
),
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
(
|
||||||
('private_key', models.TextField(blank=True, null=True)),
|
"last_login",
|
||||||
('public_key', models.TextField(blank=True, null=True)),
|
models.DateTimeField(
|
||||||
('actor', models.CharField(max_length=255, unique=True)),
|
blank=True, null=True, verbose_name="last login"
|
||||||
('inbox', models.CharField(max_length=255, unique=True)),
|
),
|
||||||
('shared_inbox', models.CharField(blank=True, max_length=255, null=True)),
|
),
|
||||||
('outbox', models.CharField(max_length=255, unique=True)),
|
(
|
||||||
('summary', models.TextField(blank=True, null=True)),
|
"is_superuser",
|
||||||
('local', models.BooleanField(default=True)),
|
models.BooleanField(
|
||||||
('fedireads_user', models.BooleanField(default=True)),
|
default=False,
|
||||||
('localname', models.CharField(max_length=255, null=True, unique=True)),
|
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||||
('name', models.CharField(blank=True, max_length=100, null=True)),
|
verbose_name="superuser status",
|
||||||
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
models.CharField(
|
||||||
|
error_messages={
|
||||||
|
"unique": "A user with that username already exists."
|
||||||
|
},
|
||||||
|
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||||
|
max_length=150,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||||
|
],
|
||||||
|
verbose_name="username",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"first_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=30, verbose_name="first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="last name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email",
|
||||||
|
models.EmailField(
|
||||||
|
blank=True, max_length=254, verbose_name="email address"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_staff",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates whether the user can log into this admin site.",
|
||||||
|
verbose_name="staff status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||||
|
verbose_name="active",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date_joined",
|
||||||
|
models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now, verbose_name="date joined"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("private_key", models.TextField(blank=True, null=True)),
|
||||||
|
("public_key", models.TextField(blank=True, null=True)),
|
||||||
|
("actor", models.CharField(max_length=255, unique=True)),
|
||||||
|
("inbox", models.CharField(max_length=255, unique=True)),
|
||||||
|
(
|
||||||
|
"shared_inbox",
|
||||||
|
models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
("outbox", models.CharField(max_length=255, unique=True)),
|
||||||
|
("summary", models.TextField(blank=True, null=True)),
|
||||||
|
("local", models.BooleanField(default=True)),
|
||||||
|
("fedireads_user", models.BooleanField(default=True)),
|
||||||
|
("localname", models.CharField(max_length=255, null=True, unique=True)),
|
||||||
|
("name", models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
(
|
||||||
|
"avatar",
|
||||||
|
models.ImageField(blank=True, null=True, upload_to="avatars/"),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'user',
|
"verbose_name": "user",
|
||||||
'verbose_name_plural': 'users',
|
"verbose_name_plural": "users",
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
managers=[
|
managers=[
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
("objects", django.contrib.auth.models.UserManager()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Author',
|
name="Author",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('content', models.TextField(blank=True, null=True)),
|
"id",
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('openlibrary_key', models.CharField(max_length=255)),
|
auto_created=True,
|
||||||
('data', JSONField()),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("content", models.TextField(blank=True, null=True)),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("openlibrary_key", models.CharField(max_length=255)),
|
||||||
|
("data", JSONField()),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Book',
|
name="Book",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('content', models.TextField(blank=True, null=True)),
|
"id",
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('openlibrary_key', models.CharField(max_length=255, unique=True)),
|
auto_created=True,
|
||||||
('data', JSONField()),
|
primary_key=True,
|
||||||
('cover', models.ImageField(blank=True, null=True, upload_to='covers/')),
|
serialize=False,
|
||||||
('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
verbose_name="ID",
|
||||||
('authors', models.ManyToManyField(to='bookwyrm.Author')),
|
),
|
||||||
|
),
|
||||||
|
("content", models.TextField(blank=True, null=True)),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("openlibrary_key", models.CharField(max_length=255, unique=True)),
|
||||||
|
("data", JSONField()),
|
||||||
|
(
|
||||||
|
"cover",
|
||||||
|
models.ImageField(blank=True, null=True, upload_to="covers/"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"added_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("authors", models.ManyToManyField(to="bookwyrm.Author")),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='FederatedServer',
|
name="FederatedServer",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('content', models.TextField(blank=True, null=True)),
|
"id",
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('server_name', models.CharField(max_length=255, unique=True)),
|
auto_created=True,
|
||||||
('status', models.CharField(default='federated', max_length=255)),
|
primary_key=True,
|
||||||
('application_type', models.CharField(max_length=255, null=True)),
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("content", models.TextField(blank=True, null=True)),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("server_name", models.CharField(max_length=255, unique=True)),
|
||||||
|
("status", models.CharField(default="federated", max_length=255)),
|
||||||
|
("application_type", models.CharField(max_length=255, null=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Shelf',
|
name="Shelf",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('content', models.TextField(blank=True, null=True)),
|
"id",
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('name', models.CharField(max_length=100)),
|
auto_created=True,
|
||||||
('identifier', models.CharField(max_length=100)),
|
primary_key=True,
|
||||||
('editable', models.BooleanField(default=True)),
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("content", models.TextField(blank=True, null=True)),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("name", models.CharField(max_length=100)),
|
||||||
|
("identifier", models.CharField(max_length=100)),
|
||||||
|
("editable", models.BooleanField(default=True)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Status',
|
name="Status",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('content', models.TextField(blank=True, null=True)),
|
"id",
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('status_type', models.CharField(default='Note', max_length=255)),
|
auto_created=True,
|
||||||
('activity_type', models.CharField(default='Note', max_length=255)),
|
primary_key=True,
|
||||||
('local', models.BooleanField(default=True)),
|
serialize=False,
|
||||||
('privacy', models.CharField(default='public', max_length=255)),
|
verbose_name="ID",
|
||||||
('sensitive', models.BooleanField(default=False)),
|
),
|
||||||
('mention_books', models.ManyToManyField(related_name='mention_book', to='bookwyrm.Book')),
|
),
|
||||||
('mention_users', models.ManyToManyField(related_name='mention_user', to=settings.AUTH_USER_MODEL)),
|
("content", models.TextField(blank=True, null=True)),
|
||||||
('reply_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status')),
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
("status_type", models.CharField(default="Note", max_length=255)),
|
||||||
|
("activity_type", models.CharField(default="Note", max_length=255)),
|
||||||
|
("local", models.BooleanField(default=True)),
|
||||||
|
("privacy", models.CharField(default="public", max_length=255)),
|
||||||
|
("sensitive", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"mention_books",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="mention_book", to="bookwyrm.Book"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mention_users",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="mention_user", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"reply_parent",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.Status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserRelationship',
|
name="UserRelationship",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('content', models.TextField(blank=True, null=True)),
|
"id",
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('status', models.CharField(default='follows', max_length=100, null=True)),
|
auto_created=True,
|
||||||
('relationship_id', models.CharField(max_length=100)),
|
primary_key=True,
|
||||||
('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_object', to=settings.AUTH_USER_MODEL)),
|
serialize=False,
|
||||||
('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_subject', to=settings.AUTH_USER_MODEL)),
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("content", models.TextField(blank=True, null=True)),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.CharField(default="follows", max_length=100, null=True),
|
||||||
|
),
|
||||||
|
("relationship_id", models.CharField(max_length=100)),
|
||||||
|
(
|
||||||
|
"user_object",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="user_object",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user_subject",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="user_subject",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ShelfBook',
|
name="ShelfBook",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('content', models.TextField(blank=True, null=True)),
|
"id",
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
auto_created=True,
|
||||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')),
|
primary_key=True,
|
||||||
('shelf', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Shelf')),
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("content", models.TextField(blank=True, null=True)),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"added_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"shelf",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Shelf"
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'unique_together': {('book', 'shelf')},
|
"unique_together": {("book", "shelf")},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='books',
|
name="books",
|
||||||
field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Book'),
|
field=models.ManyToManyField(
|
||||||
|
through="bookwyrm.ShelfBook", to="bookwyrm.Book"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='shelves',
|
name="shelves",
|
||||||
field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Shelf'),
|
field=models.ManyToManyField(
|
||||||
|
through="bookwyrm.ShelfBook", to="bookwyrm.Shelf"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='federated_server',
|
name="federated_server",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.FederatedServer'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.FederatedServer",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='followers',
|
name="followers",
|
||||||
field=models.ManyToManyField(through='bookwyrm.UserRelationship', to=settings.AUTH_USER_MODEL),
|
field=models.ManyToManyField(
|
||||||
|
through="bookwyrm.UserRelationship", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='groups',
|
name="groups",
|
||||||
field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.Group",
|
||||||
|
verbose_name="groups",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='user_permissions',
|
name="user_permissions",
|
||||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Specific permissions for this user.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.Permission",
|
||||||
|
verbose_name="user permissions",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='shelf',
|
name="shelf",
|
||||||
unique_together={('user', 'identifier')},
|
unique_together={("user", "identifier")},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Review',
|
name="Review",
|
||||||
fields=[
|
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)),
|
"status_ptr",
|
||||||
('rating', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])),
|
models.OneToOneField(
|
||||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')),
|
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)),
|
||||||
|
(
|
||||||
|
"rating",
|
||||||
|
models.IntegerField(
|
||||||
|
default=0,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(0),
|
||||||
|
django.core.validators.MaxValueValidator(5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book"
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
bases=('bookwyrm.status',),
|
bases=("bookwyrm.status",),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,31 +8,59 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0001_initial'),
|
("bookwyrm", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Favorite',
|
name="Favorite",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('content', models.TextField(blank=True, null=True)),
|
"id",
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status')),
|
auto_created=True,
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("content", models.TextField(blank=True, null=True)),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.Status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'unique_together': {('user', 'status')},
|
"unique_together": {("user", "status")},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='favorites',
|
name="favorites",
|
||||||
field=models.ManyToManyField(related_name='user_favorites', through='bookwyrm.Favorite', to=settings.AUTH_USER_MODEL),
|
field=models.ManyToManyField(
|
||||||
|
related_name="user_favorites",
|
||||||
|
through="bookwyrm.Favorite",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='favorites',
|
name="favorites",
|
||||||
field=models.ManyToManyField(related_name='favorite_statuses', through='bookwyrm.Favorite', to='bookwyrm.Status'),
|
field=models.ManyToManyField(
|
||||||
|
related_name="favorite_statuses",
|
||||||
|
through="bookwyrm.Favorite",
|
||||||
|
to="bookwyrm.Status",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,87 +7,89 @@ import django.utils.timezone
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0002_auto_20200219_0816'),
|
("bookwyrm", "0002_auto_20200219_0816"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='content',
|
name="content",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='content',
|
name="content",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='favorite',
|
model_name="favorite",
|
||||||
name='content',
|
name="content",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='federatedserver',
|
model_name="federatedserver",
|
||||||
name='content',
|
name="content",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='content',
|
name="content",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='shelfbook',
|
model_name="shelfbook",
|
||||||
name='content',
|
name="content",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='userrelationship',
|
model_name="userrelationship",
|
||||||
name='content',
|
name="content",
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='favorite',
|
model_name="favorite",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='federatedserver',
|
model_name="federatedserver",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='shelfbook',
|
model_name="shelfbook",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='created_date',
|
name="created_date",
|
||||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True, default=django.utils.timezone.now
|
||||||
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='userrelationship',
|
model_name="userrelationship",
|
||||||
name='updated_date',
|
name="updated_date",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,22 +8,41 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0003_auto_20200221_0131'),
|
("bookwyrm", "0003_auto_20200221_0131"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Tag',
|
name="Tag",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
"id",
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('name', models.CharField(max_length=140)),
|
auto_created=True,
|
||||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')),
|
primary_key=True,
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_date", models.DateTimeField(auto_now=True)),
|
||||||
|
("name", models.CharField(max_length=140)),
|
||||||
|
(
|
||||||
|
"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={
|
options={
|
||||||
'unique_together': {('user', 'book', 'name')},
|
"unique_together": {("user", "book", "name")},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,27 +5,27 @@ from django.db import migrations, models
|
||||||
|
|
||||||
def populate_identifiers(app_registry, schema_editor):
|
def populate_identifiers(app_registry, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
tags = app_registry.get_model('bookwyrm', 'Tag')
|
tags = app_registry.get_model("bookwyrm", "Tag")
|
||||||
for tag in tags.objects.using(db_alias):
|
for tag in tags.objects.using(db_alias):
|
||||||
tag.identifier = re.sub(r'\W+', '-', tag.name).lower()
|
tag.identifier = re.sub(r"\W+", "-", tag.name).lower()
|
||||||
tag.save()
|
tag.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0004_tag'),
|
("bookwyrm", "0004_tag"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='tag',
|
model_name="tag",
|
||||||
name='identifier',
|
name="identifier",
|
||||||
field=models.CharField(max_length=100, null=True),
|
field=models.CharField(max_length=100, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='tag',
|
model_name="tag",
|
||||||
name='name',
|
name="name",
|
||||||
field=models.CharField(max_length=100),
|
field=models.CharField(max_length=100),
|
||||||
),
|
),
|
||||||
migrations.RunPython(populate_identifiers),
|
migrations.RunPython(populate_identifiers),
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,13 +8,15 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0006_auto_20200221_1702_squashed_0064_merge_20201101_1913'),
|
("bookwyrm", "0006_auto_20200221_1702_squashed_0064_merge_20201101_1913"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='siteinvite',
|
model_name="siteinvite",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,8 +6,8 @@ import django.db.models.deletion
|
||||||
|
|
||||||
def set_default_edition(app_registry, schema_editor):
|
def set_default_edition(app_registry, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
works = app_registry.get_model('bookwyrm', 'Work').objects.using(db_alias)
|
works = app_registry.get_model("bookwyrm", "Work").objects.using(db_alias)
|
||||||
editions = app_registry.get_model('bookwyrm', 'Edition').objects.using(db_alias)
|
editions = app_registry.get_model("bookwyrm", "Edition").objects.using(db_alias)
|
||||||
for work in works:
|
for work in works:
|
||||||
ed = editions.filter(parent_work=work, default=True).first()
|
ed = editions.filter(parent_work=work, default=True).first()
|
||||||
if not ed:
|
if not ed:
|
||||||
|
@ -15,21 +15,26 @@ def set_default_edition(app_registry, schema_editor):
|
||||||
work.default_edition = ed
|
work.default_edition = ed
|
||||||
work.save()
|
work.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0007_auto_20201103_0014'),
|
("bookwyrm", "0007_auto_20201103_0014"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='work',
|
model_name="work",
|
||||||
name='default_edition',
|
name="default_edition",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.Edition",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(set_default_edition),
|
migrations.RunPython(set_default_edition),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='default',
|
name="default",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,22 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0008_work_default_edition'),
|
("bookwyrm", "0008_work_default_edition"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='privacy',
|
name="privacy",
|
||||||
field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("public", "Public"),
|
||||||
|
("unlisted", "Unlisted"),
|
||||||
|
("followers", "Followers"),
|
||||||
|
("direct", "Direct"),
|
||||||
|
],
|
||||||
|
default="public",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0009_shelf_privacy'),
|
("bookwyrm", "0009_shelf_privacy"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='importjob',
|
model_name="importjob",
|
||||||
name='retry',
|
name="retry",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
def set_origin_id(app_registry, schema_editor):
|
def set_origin_id(app_registry, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
books = app_registry.get_model('bookwyrm', 'Book').objects.using(db_alias)
|
books = app_registry.get_model("bookwyrm", "Book").objects.using(db_alias)
|
||||||
for book in books:
|
for book in books:
|
||||||
book.origin_id = book.remote_id
|
book.origin_id = book.remote_id
|
||||||
# the remote_id will be set automatically
|
# the remote_id will be set automatically
|
||||||
|
@ -15,18 +16,18 @@ def set_origin_id(app_registry, schema_editor):
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0010_importjob_retry'),
|
("bookwyrm", "0010_importjob_retry"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='origin_id',
|
name="origin_id",
|
||||||
field=models.CharField(max_length=255, null=True),
|
field=models.CharField(max_length=255, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='origin_id',
|
name="origin_id",
|
||||||
field=models.CharField(max_length=255, null=True),
|
field=models.CharField(max_length=255, null=True),
|
||||||
),
|
),
|
||||||
migrations.RunPython(set_origin_id),
|
migrations.RunPython(set_origin_id),
|
||||||
|
|
|
@ -7,23 +7,41 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0011_auto_20201113_1727'),
|
("bookwyrm", "0011_auto_20201113_1727"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Attachment',
|
name="Attachment",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
"id",
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('remote_id', models.CharField(max_length=255, null=True)),
|
auto_created=True,
|
||||||
('image', models.ImageField(blank=True, null=True, upload_to='status/')),
|
primary_key=True,
|
||||||
('caption', models.TextField(blank=True, null=True)),
|
serialize=False,
|
||||||
('status', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status')),
|
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={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,24 +8,51 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0011_auto_20201113_1727'),
|
("bookwyrm", "0011_auto_20201113_1727"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ProgressUpdate',
|
name="ProgressUpdate",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
"id",
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('remote_id', models.CharField(max_length=255, null=True)),
|
auto_created=True,
|
||||||
('progress', models.IntegerField()),
|
primary_key=True,
|
||||||
('mode', models.CharField(choices=[('PG', 'page'), ('PCT', 'percent')], default='PG', max_length=3)),
|
serialize=False,
|
||||||
('readthrough', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.ReadThrough')),
|
verbose_name="ID",
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
),
|
||||||
|
),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_date", models.DateTimeField(auto_now=True)),
|
||||||
|
("remote_id", models.CharField(max_length=255, null=True)),
|
||||||
|
("progress", models.IntegerField()),
|
||||||
|
(
|
||||||
|
"mode",
|
||||||
|
models.CharField(
|
||||||
|
choices=[("PG", "page"), ("PCT", "percent")],
|
||||||
|
default="PG",
|
||||||
|
max_length=3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"readthrough",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.ReadThrough",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0012_attachment'),
|
("bookwyrm", "0012_attachment"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='origin_id',
|
name="origin_id",
|
||||||
field=models.CharField(blank=True, max_length=255, null=True),
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,12 +6,12 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0013_book_origin_id'),
|
("bookwyrm", "0013_book_origin_id"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameModel(
|
migrations.RenameModel(
|
||||||
old_name='Attachment',
|
old_name="Attachment",
|
||||||
new_name='Image',
|
new_name="Image",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,9 +6,8 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0013_book_origin_id'),
|
("bookwyrm", "0013_book_origin_id"),
|
||||||
('bookwyrm', '0012_progressupdate'),
|
("bookwyrm", "0012_progressupdate"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = []
|
||||||
]
|
|
||||||
|
|
|
@ -7,13 +7,18 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0014_auto_20201128_0118'),
|
("bookwyrm", "0014_auto_20201128_0118"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='image',
|
model_name="image",
|
||||||
name='status',
|
name="status",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="attachments",
|
||||||
|
to="bookwyrm.Status",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,18 +6,20 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0014_merge_20201128_0007'),
|
("bookwyrm", "0014_merge_20201128_0007"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='readthrough',
|
model_name="readthrough",
|
||||||
old_name='pages_read',
|
old_name="pages_read",
|
||||||
new_name='progress',
|
new_name="progress",
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='readthrough',
|
model_name="readthrough",
|
||||||
name='progress_mode',
|
name="progress_mode",
|
||||||
field=models.CharField(choices=[('PG', 'page'), ('PCT', 'percent')], default='PG', max_length=3),
|
field=models.CharField(
|
||||||
|
choices=[("PG", "page"), ("PCT", "percent")], default="PG", max_length=3
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,58 +5,101 @@ from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0015_auto_20201128_0349'),
|
("bookwyrm", "0015_auto_20201128_0349"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='subject_places',
|
name="subject_places",
|
||||||
field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
field=ArrayField(
|
||||||
|
base_field=models.CharField(max_length=255),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='subjects',
|
name="subjects",
|
||||||
field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
field=ArrayField(
|
||||||
|
base_field=models.CharField(max_length=255),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='parent_work',
|
name="parent_work",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='editions', to='bookwyrm.Work'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="editions",
|
||||||
|
to="bookwyrm.Work",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='tag',
|
model_name="tag",
|
||||||
name='name',
|
name="name",
|
||||||
field=models.CharField(max_length=100, unique=True),
|
field=models.CharField(max_length=100, unique=True),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='tag',
|
name="tag",
|
||||||
unique_together=set(),
|
unique_together=set(),
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='tag',
|
model_name="tag",
|
||||||
name='book',
|
name="book",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='tag',
|
model_name="tag",
|
||||||
name='user',
|
name="user",
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserTag',
|
name="UserTag",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
"id",
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('remote_id', models.CharField(max_length=255, null=True)),
|
auto_created=True,
|
||||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')),
|
primary_key=True,
|
||||||
('tag', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Tag')),
|
serialize=False,
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
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={
|
options={
|
||||||
'unique_together': {('user', 'book', 'tag')},
|
"unique_together": {("user", "book", "tag")},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,23 +6,23 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0015_auto_20201128_0349'),
|
("bookwyrm", "0015_auto_20201128_0349"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='admin_email',
|
name="admin_email",
|
||||||
field=models.EmailField(blank=True, max_length=255, null=True),
|
field=models.EmailField(blank=True, max_length=255, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='support_link',
|
name="support_link",
|
||||||
field=models.CharField(blank=True, max_length=255, null=True),
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='support_title',
|
name="support_title",
|
||||||
field=models.CharField(blank=True, max_length=100, null=True),
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,184 +6,296 @@ from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
def copy_rsa_keys(app_registry, schema_editor):
|
def copy_rsa_keys(app_registry, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
users = app_registry.get_model('bookwyrm', 'User')
|
users = app_registry.get_model("bookwyrm", "User")
|
||||||
keypair = app_registry.get_model('bookwyrm', 'KeyPair')
|
keypair = app_registry.get_model("bookwyrm", "KeyPair")
|
||||||
for user in users.objects.using(db_alias):
|
for user in users.objects.using(db_alias):
|
||||||
if user.public_key or user.private_key:
|
if user.public_key or user.private_key:
|
||||||
user.key_pair = keypair.objects.create(
|
user.key_pair = keypair.objects.create(
|
||||||
remote_id='%s/#main-key' % user.remote_id,
|
remote_id="%s/#main-key" % user.remote_id,
|
||||||
private_key=user.private_key,
|
private_key=user.private_key,
|
||||||
public_key=user.public_key
|
public_key=user.public_key,
|
||||||
)
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0016_auto_20201129_0304'),
|
("bookwyrm", "0016_auto_20201129_0304"),
|
||||||
]
|
]
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='KeyPair',
|
name="KeyPair",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
"id",
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
|
auto_created=True,
|
||||||
('private_key', models.TextField(blank=True, null=True)),
|
primary_key=True,
|
||||||
('public_key', bookwyrm.models.fields.TextField(blank=True, null=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={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='followers',
|
name="followers",
|
||||||
field=bookwyrm.models.fields.ManyToManyField(related_name='following', through='bookwyrm.UserFollows', to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.ManyToManyField(
|
||||||
|
related_name="following",
|
||||||
|
through="bookwyrm.UserFollows",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='connector',
|
model_name="connector",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='favorite',
|
model_name="favorite",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='federatedserver',
|
model_name="federatedserver",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='image',
|
model_name="image",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='readthrough',
|
model_name="readthrough",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelfbook',
|
model_name="shelfbook",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='tag',
|
model_name="tag",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='avatar',
|
name="avatar",
|
||||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='avatars/'),
|
field=bookwyrm.models.fields.ImageField(
|
||||||
|
blank=True, null=True, upload_to="avatars/"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='bookwyrm_user',
|
name="bookwyrm_user",
|
||||||
field=bookwyrm.models.fields.BooleanField(default=True),
|
field=bookwyrm.models.fields.BooleanField(default=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='inbox',
|
name="inbox",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
unique=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='local',
|
name="local",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='manually_approves_followers',
|
name="manually_approves_followers",
|
||||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
field=bookwyrm.models.fields.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='name',
|
name="name",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=100, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=100, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='outbox',
|
name="outbox",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
unique=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='shared_inbox',
|
name="shared_inbox",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='summary',
|
name="summary",
|
||||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='username',
|
name="username",
|
||||||
field=bookwyrm.models.fields.UsernameField(),
|
field=bookwyrm.models.fields.UsernameField(),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userblocks',
|
model_name="userblocks",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userfollowrequest',
|
model_name="userfollowrequest",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userfollows',
|
model_name="userfollows",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='usertag',
|
model_name="usertag",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='key_pair',
|
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'),
|
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),
|
migrations.RunPython(copy_rsa_keys),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,13 +7,15 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0016_auto_20201211_2026'),
|
("bookwyrm", "0016_auto_20201211_2026"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='readthrough',
|
model_name="readthrough",
|
||||||
name='book',
|
name="book",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,20 +6,20 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0017_auto_20201130_1819'),
|
("bookwyrm", "0017_auto_20201130_1819"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='following',
|
name="following",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='private_key',
|
name="private_key",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='public_key',
|
name="public_key",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,34 +3,36 @@
|
||||||
import bookwyrm.models.fields
|
import bookwyrm.models.fields
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
def update_notnull(app_registry, schema_editor):
|
def update_notnull(app_registry, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
users = app_registry.get_model('bookwyrm', 'User')
|
users = app_registry.get_model("bookwyrm", "User")
|
||||||
for user in users.objects.using(db_alias):
|
for user in users.objects.using(db_alias):
|
||||||
if user.name and user.summary:
|
if user.name and user.summary:
|
||||||
continue
|
continue
|
||||||
if not user.summary:
|
if not user.summary:
|
||||||
user.summary = ''
|
user.summary = ""
|
||||||
if not user.name:
|
if not user.name:
|
||||||
user.name = ''
|
user.name = ""
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0018_auto_20201130_1832'),
|
("bookwyrm", "0018_auto_20201130_1832"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(update_notnull),
|
migrations.RunPython(update_notnull),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='name',
|
name="name",
|
||||||
field=bookwyrm.models.fields.CharField(default='', max_length=100),
|
field=bookwyrm.models.fields.CharField(default="", max_length=100),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='summary',
|
name="summary",
|
||||||
field=bookwyrm.models.fields.TextField(default=''),
|
field=bookwyrm.models.fields.TextField(default=""),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,343 +11,497 @@ import django.utils.timezone
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0019_auto_20201130_1939'),
|
("bookwyrm", "0019_auto_20201130_1939"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='aliases',
|
name="aliases",
|
||||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
field=bookwyrm.models.fields.ArrayField(
|
||||||
|
base_field=models.CharField(max_length=255),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='bio',
|
name="bio",
|
||||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='born',
|
name="born",
|
||||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='died',
|
name="died",
|
||||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='name',
|
name="name",
|
||||||
field=bookwyrm.models.fields.CharField(max_length=255),
|
field=bookwyrm.models.fields.CharField(max_length=255),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='openlibrary_key',
|
name="openlibrary_key",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='wikipedia_link',
|
name="wikipedia_link",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='authors',
|
name="authors",
|
||||||
field=bookwyrm.models.fields.ManyToManyField(to='bookwyrm.Author'),
|
field=bookwyrm.models.fields.ManyToManyField(to="bookwyrm.Author"),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='cover',
|
name="cover",
|
||||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='covers/'),
|
field=bookwyrm.models.fields.ImageField(
|
||||||
|
blank=True, null=True, upload_to="covers/"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='description',
|
name="description",
|
||||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='first_published_date',
|
name="first_published_date",
|
||||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='goodreads_key',
|
name="goodreads_key",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='languages',
|
name="languages",
|
||||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
field=bookwyrm.models.fields.ArrayField(
|
||||||
|
base_field=models.CharField(max_length=255),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='librarything_key',
|
name="librarything_key",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='openlibrary_key',
|
name="openlibrary_key",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='published_date',
|
name="published_date",
|
||||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='series',
|
name="series",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='series_number',
|
name="series_number",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='sort_title',
|
name="sort_title",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='subject_places',
|
name="subject_places",
|
||||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
field=bookwyrm.models.fields.ArrayField(
|
||||||
|
base_field=models.CharField(max_length=255),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='subjects',
|
name="subjects",
|
||||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
field=bookwyrm.models.fields.ArrayField(
|
||||||
|
base_field=models.CharField(max_length=255),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='subtitle',
|
name="subtitle",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='title',
|
name="title",
|
||||||
field=bookwyrm.models.fields.CharField(max_length=255),
|
field=bookwyrm.models.fields.CharField(max_length=255),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='boost',
|
model_name="boost",
|
||||||
name='boosted_status',
|
name="boosted_status",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boosters', to='bookwyrm.Status'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="boosters",
|
||||||
|
to="bookwyrm.Status",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='comment',
|
model_name="comment",
|
||||||
name='book',
|
name="book",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='asin',
|
name="asin",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='isbn_10',
|
name="isbn_10",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='isbn_13',
|
name="isbn_13",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='oclc_number',
|
name="oclc_number",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='pages',
|
name="pages",
|
||||||
field=bookwyrm.models.fields.IntegerField(blank=True, null=True),
|
field=bookwyrm.models.fields.IntegerField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='parent_work',
|
name="parent_work",
|
||||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='editions', to='bookwyrm.Work'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="editions",
|
||||||
|
to="bookwyrm.Work",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='physical_format',
|
name="physical_format",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='publishers',
|
name="publishers",
|
||||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
field=bookwyrm.models.fields.ArrayField(
|
||||||
|
base_field=models.CharField(max_length=255),
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='favorite',
|
model_name="favorite",
|
||||||
name='status',
|
name="status",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Status"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='favorite',
|
model_name="favorite",
|
||||||
name='user',
|
name="user",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='image',
|
model_name="image",
|
||||||
name='caption',
|
name="caption",
|
||||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='image',
|
model_name="image",
|
||||||
name='image',
|
name="image",
|
||||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='status/'),
|
field=bookwyrm.models.fields.ImageField(
|
||||||
|
blank=True, null=True, upload_to="status/"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='quotation',
|
model_name="quotation",
|
||||||
name='book',
|
name="book",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='quotation',
|
model_name="quotation",
|
||||||
name='quote',
|
name="quote",
|
||||||
field=bookwyrm.models.fields.TextField(),
|
field=bookwyrm.models.fields.TextField(),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='review',
|
model_name="review",
|
||||||
name='book',
|
name="book",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='review',
|
model_name="review",
|
||||||
name='name',
|
name="name",
|
||||||
field=bookwyrm.models.fields.CharField(max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(max_length=255, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='review',
|
model_name="review",
|
||||||
name='rating',
|
name="rating",
|
||||||
field=bookwyrm.models.fields.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]),
|
field=bookwyrm.models.fields.IntegerField(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(5),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='name',
|
name="name",
|
||||||
field=bookwyrm.models.fields.CharField(max_length=100),
|
field=bookwyrm.models.fields.CharField(max_length=100),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='privacy',
|
name="privacy",
|
||||||
field=bookwyrm.models.fields.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
choices=[
|
||||||
|
("public", "Public"),
|
||||||
|
("unlisted", "Unlisted"),
|
||||||
|
("followers", "Followers"),
|
||||||
|
("direct", "Direct"),
|
||||||
|
],
|
||||||
|
default="public",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='user',
|
name="user",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelfbook',
|
model_name="shelfbook",
|
||||||
name='added_by',
|
name="added_by",
|
||||||
field=bookwyrm.models.fields.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelfbook',
|
model_name="shelfbook",
|
||||||
name='book',
|
name="book",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelfbook',
|
model_name="shelfbook",
|
||||||
name='shelf',
|
name="shelf",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Shelf'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Shelf"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='content',
|
name="content",
|
||||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='mention_books',
|
name="mention_books",
|
||||||
field=bookwyrm.models.fields.TagField(related_name='mention_book', to='bookwyrm.Edition'),
|
field=bookwyrm.models.fields.TagField(
|
||||||
|
related_name="mention_book", to="bookwyrm.Edition"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='mention_users',
|
name="mention_users",
|
||||||
field=bookwyrm.models.fields.TagField(related_name='mention_user', to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.TagField(
|
||||||
|
related_name="mention_user", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='published_date',
|
name="published_date",
|
||||||
field=bookwyrm.models.fields.DateTimeField(default=django.utils.timezone.now),
|
field=bookwyrm.models.fields.DateTimeField(
|
||||||
|
default=django.utils.timezone.now
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='reply_parent',
|
name="reply_parent",
|
||||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.Status",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='sensitive',
|
name="sensitive",
|
||||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
field=bookwyrm.models.fields.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='user',
|
name="user",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='tag',
|
model_name="tag",
|
||||||
name='name',
|
name="name",
|
||||||
field=bookwyrm.models.fields.CharField(max_length=100, unique=True),
|
field=bookwyrm.models.fields.CharField(max_length=100, unique=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userblocks',
|
model_name="userblocks",
|
||||||
name='user_object',
|
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),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="userblocks_user_object",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userblocks',
|
model_name="userblocks",
|
||||||
name='user_subject',
|
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),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="userblocks_user_subject",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userfollowrequest',
|
model_name="userfollowrequest",
|
||||||
name='user_object',
|
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),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="userfollowrequest_user_object",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userfollowrequest',
|
model_name="userfollowrequest",
|
||||||
name='user_subject',
|
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),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="userfollowrequest_user_subject",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userfollows',
|
model_name="userfollows",
|
||||||
name='user_object',
|
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),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="userfollows_user_object",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userfollows',
|
model_name="userfollows",
|
||||||
name='user_subject',
|
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),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="userfollows_user_subject",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='usertag',
|
model_name="usertag",
|
||||||
name='book',
|
name="book",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='usertag',
|
model_name="usertag",
|
||||||
name='tag',
|
name="tag",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Tag'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Tag"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='usertag',
|
model_name="usertag",
|
||||||
name='user',
|
name="user",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='work',
|
model_name="work",
|
||||||
name='default_edition',
|
name="default_edition",
|
||||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.Edition",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='work',
|
model_name="work",
|
||||||
name='lccn',
|
name="lccn",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,9 +6,8 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0020_auto_20201208_0213'),
|
("bookwyrm", "0020_auto_20201208_0213"),
|
||||||
('bookwyrm', '0016_auto_20201211_2026'),
|
("bookwyrm", "0016_auto_20201211_2026"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = []
|
||||||
]
|
|
||||||
|
|
|
@ -5,26 +5,27 @@ from django.db import migrations
|
||||||
|
|
||||||
def set_author_name(app_registry, schema_editor):
|
def set_author_name(app_registry, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
authors = app_registry.get_model('bookwyrm', 'Author')
|
authors = app_registry.get_model("bookwyrm", "Author")
|
||||||
for author in authors.objects.using(db_alias):
|
for author in authors.objects.using(db_alias):
|
||||||
if not author.name:
|
if not author.name:
|
||||||
author.name = '%s %s' % (author.first_name, author.last_name)
|
author.name = "%s %s" % (author.first_name, author.last_name)
|
||||||
author.save()
|
author.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0021_merge_20201212_1737'),
|
("bookwyrm", "0021_merge_20201212_1737"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(set_author_name),
|
migrations.RunPython(set_author_name),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='first_name',
|
name="first_name",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='last_name',
|
name="last_name",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,13 +7,22 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0022_auto_20201212_1744'),
|
("bookwyrm", "0022_auto_20201212_1744"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='privacy',
|
name="privacy",
|
||||||
field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
field=bookwyrm.models.fields.PrivacyField(
|
||||||
|
choices=[
|
||||||
|
("public", "Public"),
|
||||||
|
("unlisted", "Unlisted"),
|
||||||
|
("followers", "Followers"),
|
||||||
|
("direct", "Direct"),
|
||||||
|
],
|
||||||
|
default="public",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,9 +6,8 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0017_auto_20201212_0059'),
|
("bookwyrm", "0017_auto_20201212_0059"),
|
||||||
('bookwyrm', '0022_auto_20201212_1744'),
|
("bookwyrm", "0022_auto_20201212_1744"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = []
|
||||||
]
|
|
||||||
|
|
|
@ -6,9 +6,8 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0023_auto_20201214_0511'),
|
("bookwyrm", "0023_auto_20201214_0511"),
|
||||||
('bookwyrm', '0023_merge_20201216_0112'),
|
("bookwyrm", "0023_merge_20201216_0112"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = []
|
||||||
]
|
|
||||||
|
|
|
@ -7,33 +7,33 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0024_merge_20201216_1721'),
|
("bookwyrm", "0024_merge_20201216_1721"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='bio',
|
name="bio",
|
||||||
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='description',
|
name="description",
|
||||||
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='quotation',
|
model_name="quotation",
|
||||||
name='quote',
|
name="quote",
|
||||||
field=bookwyrm.models.fields.HtmlField(),
|
field=bookwyrm.models.fields.HtmlField(),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='content',
|
name="content",
|
||||||
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='summary',
|
name="summary",
|
||||||
field=bookwyrm.models.fields.HtmlField(default=''),
|
field=bookwyrm.models.fields.HtmlField(default=""),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,13 +7,15 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0025_auto_20201217_0046'),
|
("bookwyrm", "0025_auto_20201217_0046"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='status',
|
model_name="status",
|
||||||
name='content_warning',
|
name="content_warning",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=500, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=500, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,18 +7,20 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0026_status_content_warning'),
|
("bookwyrm", "0026_status_content_warning"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='name',
|
name="name",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=100, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=100, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='summary',
|
name="summary",
|
||||||
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,12 +6,12 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0027_auto_20201220_2007'),
|
("bookwyrm", "0027_auto_20201220_2007"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='author_text',
|
name="author_text",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,53 +9,65 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0028_remove_book_author_text'),
|
("bookwyrm", "0028_remove_book_author_text"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='last_sync_date',
|
name="last_sync_date",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='sync',
|
name="sync",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='last_sync_date',
|
name="last_sync_date",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='sync',
|
name="sync",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='sync_cover',
|
name="sync_cover",
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='goodreads_key',
|
name="goodreads_key",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='last_edited_by',
|
name="last_edited_by",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='librarything_key',
|
name="librarything_key",
|
||||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True, max_length=255, null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='book',
|
model_name="book",
|
||||||
name='last_edited_by',
|
name="last_edited_by",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='author',
|
model_name="author",
|
||||||
name='origin_id',
|
name="origin_id",
|
||||||
field=models.CharField(blank=True, max_length=255, null=True),
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,13 +7,18 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0029_auto_20201221_2014'),
|
("bookwyrm", "0029_auto_20201221_2014"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='localname',
|
name="localname",
|
||||||
field=models.CharField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_localname]),
|
field=models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_localname],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,23 +6,23 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0030_auto_20201224_1939'),
|
("bookwyrm", "0030_auto_20201224_1939"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='favicon',
|
name="favicon",
|
||||||
field=models.ImageField(blank=True, null=True, upload_to='logos/'),
|
field=models.ImageField(blank=True, null=True, upload_to="logos/"),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='logo',
|
name="logo",
|
||||||
field=models.ImageField(blank=True, null=True, upload_to='logos/'),
|
field=models.ImageField(blank=True, null=True, upload_to="logos/"),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='logo_small',
|
name="logo_small",
|
||||||
field=models.ImageField(blank=True, null=True, upload_to='logos/'),
|
field=models.ImageField(blank=True, null=True, upload_to="logos/"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,18 +6,20 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0031_auto_20210104_2040'),
|
("bookwyrm", "0031_auto_20210104_2040"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='instance_tagline',
|
name="instance_tagline",
|
||||||
field=models.CharField(default='Social Reading and Reviewing', max_length=150),
|
field=models.CharField(
|
||||||
|
default="Social Reading and Reviewing", max_length=150
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='registration_closed_text',
|
name="registration_closed_text",
|
||||||
field=models.TextField(default='Contact an administrator to get an invite'),
|
field=models.TextField(default="Contact an administrator to get an invite"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,14 +7,16 @@ import django.utils.timezone
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0032_auto_20210104_2055'),
|
("bookwyrm", "0032_auto_20210104_2055"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='siteinvite',
|
model_name="siteinvite",
|
||||||
name='created_date',
|
name="created_date",
|
||||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True, default=django.utils.timezone.now
|
||||||
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0033_siteinvite_created_date'),
|
("bookwyrm", "0033_siteinvite_created_date"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='importjob',
|
model_name="importjob",
|
||||||
name='complete',
|
name="complete",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,20 +6,21 @@ from django.db import migrations
|
||||||
|
|
||||||
def set_rank(app_registry, schema_editor):
|
def set_rank(app_registry, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
books = app_registry.get_model('bookwyrm', 'Edition')
|
books = app_registry.get_model("bookwyrm", "Edition")
|
||||||
for book in books.objects.using(db_alias):
|
for book in books.objects.using(db_alias):
|
||||||
book.save()
|
book.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0034_importjob_complete'),
|
("bookwyrm", "0034_importjob_complete"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='edition',
|
model_name="edition",
|
||||||
name='edition_rank',
|
name="edition_rank",
|
||||||
field=bookwyrm.models.fields.IntegerField(default=0),
|
field=bookwyrm.models.fields.IntegerField(default=0),
|
||||||
),
|
),
|
||||||
migrations.RunPython(set_rank),
|
migrations.RunPython(set_rank),
|
||||||
|
|
|
@ -9,24 +9,57 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0035_edition_edition_rank'),
|
("bookwyrm", "0035_edition_edition_rank"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='AnnualGoal',
|
name="AnnualGoal",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
"id",
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
|
auto_created=True,
|
||||||
('goal', models.IntegerField()),
|
primary_key=True,
|
||||||
('year', models.IntegerField(default=2021)),
|
serialize=False,
|
||||||
('privacy', models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)),
|
verbose_name="ID",
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
),
|
||||||
|
),
|
||||||
|
("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],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("goal", models.IntegerField()),
|
||||||
|
("year", models.IntegerField(default=2021)),
|
||||||
|
(
|
||||||
|
"privacy",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("public", "Public"),
|
||||||
|
("unlisted", "Unlisted"),
|
||||||
|
("followers", "Followers"),
|
||||||
|
("direct", "Direct"),
|
||||||
|
],
|
||||||
|
default="public",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'unique_together': {('user', 'year')},
|
"unique_together": {("user", "year")},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,36 +2,39 @@
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
def empty_to_null(apps, schema_editor):
|
def empty_to_null(apps, schema_editor):
|
||||||
User = apps.get_model("bookwyrm", "User")
|
User = apps.get_model("bookwyrm", "User")
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
User.objects.using(db_alias).filter(email="").update(email=None)
|
User.objects.using(db_alias).filter(email="").update(email=None)
|
||||||
|
|
||||||
|
|
||||||
def null_to_empty(apps, schema_editor):
|
def null_to_empty(apps, schema_editor):
|
||||||
User = apps.get_model("bookwyrm", "User")
|
User = apps.get_model("bookwyrm", "User")
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
User.objects.using(db_alias).filter(email=None).update(email="")
|
User.objects.using(db_alias).filter(email=None).update(email="")
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0036_annualgoal'),
|
("bookwyrm", "0036_annualgoal"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='shelfbook',
|
name="shelfbook",
|
||||||
options={'ordering': ('-created_date',)},
|
options={"ordering": ("-created_date",)},
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='email',
|
name="email",
|
||||||
field=models.EmailField(max_length=254, null=True),
|
field=models.EmailField(max_length=254, null=True),
|
||||||
),
|
),
|
||||||
migrations.RunPython(empty_to_null, null_to_empty),
|
migrations.RunPython(empty_to_null, null_to_empty),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user',
|
model_name="user",
|
||||||
name='email',
|
name="email",
|
||||||
field=models.EmailField(max_length=254, null=True, unique=True),
|
field=models.EmailField(max_length=254, null=True, unique=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,13 +7,15 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0037_auto_20210118_1954'),
|
("bookwyrm", "0037_auto_20210118_1954"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='annualgoal',
|
model_name="annualgoal",
|
||||||
name='goal',
|
name="goal",
|
||||||
field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)]),
|
field=models.IntegerField(
|
||||||
|
validators=[django.core.validators.MinValueValidator(1)]
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,9 +6,8 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0038_auto_20210119_1534'),
|
("bookwyrm", "0038_auto_20210119_1534"),
|
||||||
('bookwyrm', '0015_auto_20201128_0734'),
|
("bookwyrm", "0015_auto_20201128_0734"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = []
|
||||||
]
|
|
||||||
|
|
|
@ -9,28 +9,40 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0039_merge_20210120_0753'),
|
("bookwyrm", "0039_merge_20210120_0753"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='progressupdate',
|
model_name="progressupdate",
|
||||||
name='progress',
|
name="progress",
|
||||||
field=models.IntegerField(validators=[django.core.validators.MinValueValidator(0)]),
|
field=models.IntegerField(
|
||||||
|
validators=[django.core.validators.MinValueValidator(0)]
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='progressupdate',
|
model_name="progressupdate",
|
||||||
name='readthrough',
|
name="readthrough",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ReadThrough'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="bookwyrm.ReadThrough"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='progressupdate',
|
model_name="progressupdate",
|
||||||
name='remote_id',
|
name="remote_id",
|
||||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
field=bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='readthrough',
|
model_name="readthrough",
|
||||||
name='progress',
|
name="progress",
|
||||||
field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)]),
|
field=models.IntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[django.core.validators.MinValueValidator(0)],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,56 +10,141 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0040_auto_20210122_0057'),
|
("bookwyrm", "0040_auto_20210122_0057"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='List',
|
name="List",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
"id",
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
|
auto_created=True,
|
||||||
('name', bookwyrm.models.fields.CharField(max_length=100)),
|
primary_key=True,
|
||||||
('description', bookwyrm.models.fields.TextField(blank=True, null=True)),
|
serialize=False,
|
||||||
('privacy', bookwyrm.models.fields.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)),
|
verbose_name="ID",
|
||||||
('curation', bookwyrm.models.fields.CharField(choices=[('closed', 'Closed'), ('open', 'Open'), ('curated', 'Curated')], default='closed', max_length=255)),
|
),
|
||||||
|
),
|
||||||
|
("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],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", bookwyrm.models.fields.CharField(max_length=100)),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"privacy",
|
||||||
|
bookwyrm.models.fields.CharField(
|
||||||
|
choices=[
|
||||||
|
("public", "Public"),
|
||||||
|
("unlisted", "Unlisted"),
|
||||||
|
("followers", "Followers"),
|
||||||
|
("direct", "Direct"),
|
||||||
|
],
|
||||||
|
default="public",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"curation",
|
||||||
|
bookwyrm.models.fields.CharField(
|
||||||
|
choices=[
|
||||||
|
("closed", "Closed"),
|
||||||
|
("open", "Open"),
|
||||||
|
("curated", "Curated"),
|
||||||
|
],
|
||||||
|
default="closed",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
bases=(bookwyrm.models.activitypub_mixin.OrderedCollectionMixin, models.Model),
|
bases=(
|
||||||
|
bookwyrm.models.activitypub_mixin.OrderedCollectionMixin,
|
||||||
|
models.Model,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ListItem',
|
name="ListItem",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
"id",
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
|
auto_created=True,
|
||||||
('notes', bookwyrm.models.fields.TextField(blank=True, null=True)),
|
primary_key=True,
|
||||||
('approved', models.BooleanField(default=True)),
|
serialize=False,
|
||||||
('order', bookwyrm.models.fields.IntegerField(blank=True, null=True)),
|
verbose_name="ID",
|
||||||
('added_by', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
),
|
||||||
('book', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')),
|
),
|
||||||
('book_list', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.List')),
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
('endorsement', models.ManyToManyField(related_name='endorsers', to=settings.AUTH_USER_MODEL)),
|
("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],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("notes", bookwyrm.models.fields.TextField(blank=True, null=True)),
|
||||||
|
("approved", models.BooleanField(default=True)),
|
||||||
|
("order", bookwyrm.models.fields.IntegerField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"added_by",
|
||||||
|
bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.Edition",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"book_list",
|
||||||
|
bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="bookwyrm.List"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"endorsement",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="endorsers", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('-created_date',),
|
"ordering": ("-created_date",),
|
||||||
'unique_together': {('book', 'book_list')},
|
"unique_together": {("book", "book_list")},
|
||||||
},
|
},
|
||||||
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='list',
|
model_name="list",
|
||||||
name='books',
|
name="books",
|
||||||
field=models.ManyToManyField(through='bookwyrm.ListItem', to='bookwyrm.Edition'),
|
field=models.ManyToManyField(
|
||||||
|
through="bookwyrm.ListItem", to="bookwyrm.Edition"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='list',
|
model_name="list",
|
||||||
name='user',
|
name="user",
|
||||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,22 +7,40 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0041_auto_20210131_1614'),
|
("bookwyrm", "0041_auto_20210131_1614"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='list',
|
name="list",
|
||||||
options={'ordering': ('-updated_date',)},
|
options={"ordering": ("-updated_date",)},
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='list',
|
model_name="list",
|
||||||
name='privacy',
|
name="privacy",
|
||||||
field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
field=bookwyrm.models.fields.PrivacyField(
|
||||||
|
choices=[
|
||||||
|
("public", "Public"),
|
||||||
|
("unlisted", "Unlisted"),
|
||||||
|
("followers", "Followers"),
|
||||||
|
("direct", "Direct"),
|
||||||
|
],
|
||||||
|
default="public",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelf',
|
model_name="shelf",
|
||||||
name='privacy',
|
name="privacy",
|
||||||
field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
field=bookwyrm.models.fields.PrivacyField(
|
||||||
|
choices=[
|
||||||
|
("public", "Public"),
|
||||||
|
("unlisted", "Unlisted"),
|
||||||
|
("followers", "Followers"),
|
||||||
|
("direct", "Direct"),
|
||||||
|
],
|
||||||
|
default="public",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,18 +6,18 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0042_auto_20210201_2108'),
|
("bookwyrm", "0042_auto_20210201_2108"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='listitem',
|
model_name="listitem",
|
||||||
old_name='added_by',
|
old_name="added_by",
|
||||||
new_name='user',
|
new_name="user",
|
||||||
),
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='shelfbook',
|
model_name="shelfbook",
|
||||||
old_name='added_by',
|
old_name="added_by",
|
||||||
new_name='user',
|
new_name="user",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,9 +5,10 @@ from django.conf import settings
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
def set_user(app_registry, schema_editor):
|
def set_user(app_registry, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
shelfbook = app_registry.get_model('bookwyrm', 'ShelfBook')
|
shelfbook = app_registry.get_model("bookwyrm", "ShelfBook")
|
||||||
for item in shelfbook.objects.using(db_alias).filter(user__isnull=True):
|
for item in shelfbook.objects.using(db_alias).filter(user__isnull=True):
|
||||||
item.user = item.shelf.user
|
item.user = item.shelf.user
|
||||||
try:
|
try:
|
||||||
|
@ -19,15 +20,19 @@ def set_user(app_registry, schema_editor):
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0043_auto_20210204_2223'),
|
("bookwyrm", "0043_auto_20210204_2223"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(set_user, lambda x, y: None),
|
migrations.RunPython(set_user, lambda x, y: None),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='shelfbook',
|
model_name="shelfbook",
|
||||||
name='user',
|
name="user",
|
||||||
field=bookwyrm.models.fields.ForeignKey(default=2, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
default=2,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,51 +8,102 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0044_auto_20210207_1924'),
|
("bookwyrm", "0044_auto_20210207_1924"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveConstraint(
|
migrations.RemoveConstraint(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='notification_type_valid',
|
name="notification_type_valid",
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='related_list_item',
|
name="related_list_item",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ListItem'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="bookwyrm.ListItem",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='notification_type',
|
name="notification_type",
|
||||||
field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import'), ('ADD', 'Add')], max_length=255),
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("FAVORITE", "Favorite"),
|
||||||
|
("REPLY", "Reply"),
|
||||||
|
("MENTION", "Mention"),
|
||||||
|
("TAG", "Tag"),
|
||||||
|
("FOLLOW", "Follow"),
|
||||||
|
("FOLLOW_REQUEST", "Follow Request"),
|
||||||
|
("BOOST", "Boost"),
|
||||||
|
("IMPORT", "Import"),
|
||||||
|
("ADD", "Add"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='related_book',
|
name="related_book",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.Edition'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="bookwyrm.Edition",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='related_import',
|
name="related_import",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ImportJob'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="bookwyrm.ImportJob",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='related_status',
|
name="related_status",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.Status'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="bookwyrm.Status",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='related_user',
|
name="related_user",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_user', to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="related_user",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='notification',
|
model_name="notification",
|
||||||
constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT', 'ADD']), name='notification_type_valid'),
|
constraint=models.CheckConstraint(
|
||||||
|
check=models.Q(
|
||||||
|
notification_type__in=[
|
||||||
|
"FAVORITE",
|
||||||
|
"REPLY",
|
||||||
|
"MENTION",
|
||||||
|
"TAG",
|
||||||
|
"FOLLOW",
|
||||||
|
"FOLLOW_REQUEST",
|
||||||
|
"BOOST",
|
||||||
|
"IMPORT",
|
||||||
|
"ADD",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
name="notification_type_valid",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('bookwyrm', '0045_auto_20210210_2114'),
|
("bookwyrm", "0045_auto_20210210_2114"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sitesettings',
|
model_name="sitesettings",
|
||||||
name='privacy_policy',
|
name="privacy_policy",
|
||||||
field=models.TextField(default='Add a privacy policy here.'),
|
field=models.TextField(default="Add a privacy policy here."),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
18
bookwyrm/migrations/0047_connector_isbn_search_url.py
Normal file
18
bookwyrm/migrations/0047_connector_isbn_search_url.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-02-28 16:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0046_sitesettings_privacy_policy"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="connector",
|
||||||
|
name="isbn_search_url",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,4 +1,4 @@
|
||||||
''' bring all the models into the app namespace '''
|
""" bring all the models into the app namespace """
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -28,8 +28,12 @@ from .import_job import ImportJob, ImportItem
|
||||||
from .site import SiteSettings, SiteInvite, PasswordReset
|
from .site import SiteSettings, SiteInvite, PasswordReset
|
||||||
|
|
||||||
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
||||||
activity_models = {c[1].activity_serializer.__name__: c[1] \
|
activity_models = {
|
||||||
for c in cls_members if hasattr(c[1], 'activity_serializer')}
|
c[1].activity_serializer.__name__: c[1]
|
||||||
|
for c in cls_members
|
||||||
|
if hasattr(c[1], "activity_serializer")
|
||||||
|
}
|
||||||
|
|
||||||
status_models = [
|
status_models = [
|
||||||
c.__name__ for (_, c) in activity_models.items() if issubclass(c, Status)]
|
c.__name__ for (_, c) in activity_models.items() if issubclass(c, Status)
|
||||||
|
]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' activitypub model functionality '''
|
""" activitypub model functionality """
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import json
|
import json
|
||||||
|
@ -26,18 +26,19 @@ logger = logging.getLogger(__name__)
|
||||||
# I tried to separate these classes into mutliple files but I kept getting
|
# I tried to separate these classes into mutliple files but I kept getting
|
||||||
# circular import errors so I gave up. I'm sure it could be done though!
|
# circular import errors so I gave up. I'm sure it could be done though!
|
||||||
class ActivitypubMixin:
|
class ActivitypubMixin:
|
||||||
''' add this mixin for models that are AP serializable '''
|
""" add this mixin for models that are AP serializable """
|
||||||
|
|
||||||
activity_serializer = lambda: {}
|
activity_serializer = lambda: {}
|
||||||
reverse_unfurl = False
|
reverse_unfurl = False
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
''' collect some info on model fields '''
|
""" collect some info on model fields """
|
||||||
self.image_fields = []
|
self.image_fields = []
|
||||||
self.many_to_many_fields = []
|
self.many_to_many_fields = []
|
||||||
self.simple_fields = [] # "simple"
|
self.simple_fields = [] # "simple"
|
||||||
# sort model fields by type
|
# sort model fields by type
|
||||||
for field in self._meta.get_fields():
|
for field in self._meta.get_fields():
|
||||||
if not hasattr(field, 'field_to_activity'):
|
if not hasattr(field, "field_to_activity"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(field, ImageField):
|
if isinstance(field, ImageField):
|
||||||
|
@ -48,33 +49,41 @@ class ActivitypubMixin:
|
||||||
self.simple_fields.append(field)
|
self.simple_fields.append(field)
|
||||||
|
|
||||||
# a list of allll the serializable fields
|
# a list of allll the serializable fields
|
||||||
self.activity_fields = self.image_fields + \
|
self.activity_fields = (
|
||||||
self.many_to_many_fields + self.simple_fields
|
self.image_fields + self.many_to_many_fields + self.simple_fields
|
||||||
|
)
|
||||||
|
|
||||||
# these are separate to avoid infinite recursion issues
|
# these are separate to avoid infinite recursion issues
|
||||||
self.deserialize_reverse_fields = self.deserialize_reverse_fields \
|
self.deserialize_reverse_fields = (
|
||||||
if hasattr(self, 'deserialize_reverse_fields') else []
|
self.deserialize_reverse_fields
|
||||||
self.serialize_reverse_fields = self.serialize_reverse_fields \
|
if hasattr(self, "deserialize_reverse_fields")
|
||||||
if hasattr(self, 'serialize_reverse_fields') else []
|
else []
|
||||||
|
)
|
||||||
|
self.serialize_reverse_fields = (
|
||||||
|
self.serialize_reverse_fields
|
||||||
|
if hasattr(self, "serialize_reverse_fields")
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_existing_by_remote_id(cls, remote_id):
|
def find_existing_by_remote_id(cls, remote_id):
|
||||||
''' look up a remote id in the db '''
|
""" look up a remote id in the db """
|
||||||
return cls.find_existing({'id': remote_id})
|
return cls.find_existing({"id": remote_id})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_existing(cls, data):
|
def find_existing(cls, data):
|
||||||
''' compare data to fields that can be used for deduplation.
|
"""compare data to fields that can be used for deduplation.
|
||||||
This always includes remote_id, but can also be unique identifiers
|
This always includes remote_id, but can also be unique identifiers
|
||||||
like an isbn for an edition '''
|
like an isbn for an edition"""
|
||||||
filters = []
|
filters = []
|
||||||
# grabs all the data from the model to create django queryset filters
|
# grabs all the data from the model to create django queryset filters
|
||||||
for field in cls._meta.get_fields():
|
for field in cls._meta.get_fields():
|
||||||
if not hasattr(field, 'deduplication_field') or \
|
if (
|
||||||
not field.deduplication_field:
|
not hasattr(field, "deduplication_field")
|
||||||
|
or not field.deduplication_field
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
value = data.get(field.get_activitypub_field())
|
value = data.get(field.get_activitypub_field())
|
||||||
|
@ -82,9 +91,9 @@ class ActivitypubMixin:
|
||||||
continue
|
continue
|
||||||
filters.append({field.name: value})
|
filters.append({field.name: value})
|
||||||
|
|
||||||
if hasattr(cls, 'origin_id') and 'id' in data:
|
if hasattr(cls, "origin_id") and "id" in data:
|
||||||
# kinda janky, but this handles special case for books
|
# kinda janky, but this handles special case for books
|
||||||
filters.append({'origin_id': data['id']})
|
filters.append({"origin_id": data["id"]})
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
# if there are no deduplication fields, it will match the first
|
# if there are no deduplication fields, it will match the first
|
||||||
|
@ -92,45 +101,41 @@ class ActivitypubMixin:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
objects = cls.objects
|
objects = cls.objects
|
||||||
if hasattr(objects, 'select_subclasses'):
|
if hasattr(objects, "select_subclasses"):
|
||||||
objects = objects.select_subclasses()
|
objects = objects.select_subclasses()
|
||||||
|
|
||||||
# an OR operation on all the match fields, sorry for the dense syntax
|
# an OR operation on all the match fields, sorry for the dense syntax
|
||||||
match = objects.filter(
|
match = objects.filter(reduce(operator.or_, (Q(**f) for f in filters)))
|
||||||
reduce(operator.or_, (Q(**f) for f in filters))
|
|
||||||
)
|
|
||||||
# there OUGHT to be only one match
|
# there OUGHT to be only one match
|
||||||
return match.first()
|
return match.first()
|
||||||
|
|
||||||
|
|
||||||
def broadcast(self, activity, sender, software=None):
|
def broadcast(self, activity, sender, software=None):
|
||||||
''' send out an activity '''
|
""" send out an activity """
|
||||||
broadcast_task.delay(
|
broadcast_task.delay(
|
||||||
sender.id,
|
sender.id,
|
||||||
json.dumps(activity, cls=activitypub.ActivityEncoder),
|
json.dumps(activity, cls=activitypub.ActivityEncoder),
|
||||||
self.get_recipients(software=software)
|
self.get_recipients(software=software),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_recipients(self, software=None):
|
def get_recipients(self, software=None):
|
||||||
''' figure out which inbox urls to post to '''
|
""" figure out which inbox urls to post to """
|
||||||
# first we have to figure out who should receive this activity
|
# first we have to figure out who should receive this activity
|
||||||
privacy = self.privacy if hasattr(self, 'privacy') else 'public'
|
privacy = self.privacy if hasattr(self, "privacy") else "public"
|
||||||
# is this activity owned by a user (statuses, lists, shelves), or is it
|
# is this activity owned by a user (statuses, lists, shelves), or is it
|
||||||
# general to the instance (like books)
|
# general to the instance (like books)
|
||||||
user = self.user if hasattr(self, 'user') else None
|
user = self.user if hasattr(self, "user") else None
|
||||||
user_model = apps.get_model('bookwyrm.User', require_ready=True)
|
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
||||||
if not user and isinstance(self, user_model):
|
if not user and isinstance(self, user_model):
|
||||||
# or maybe the thing itself is a user
|
# or maybe the thing itself is a user
|
||||||
user = self
|
user = self
|
||||||
# find anyone who's tagged in a status, for example
|
# find anyone who's tagged in a status, for example
|
||||||
mentions = self.recipients if hasattr(self, 'recipients') else []
|
mentions = self.recipients if hasattr(self, "recipients") else []
|
||||||
|
|
||||||
# we always send activities to explicitly mentioned users' inboxes
|
# we always send activities to explicitly mentioned users' inboxes
|
||||||
recipients = [u.inbox for u in mentions or []]
|
recipients = [u.inbox for u in mentions or []]
|
||||||
|
|
||||||
# unless it's a dm, all the followers should receive the activity
|
# unless it's a dm, all the followers should receive the activity
|
||||||
if privacy != 'direct':
|
if privacy != "direct":
|
||||||
# we will send this out to a subset of all remote users
|
# we will send this out to a subset of all remote users
|
||||||
queryset = user_model.objects.filter(
|
queryset = user_model.objects.filter(
|
||||||
local=False,
|
local=False,
|
||||||
|
@ -138,43 +143,43 @@ class ActivitypubMixin:
|
||||||
# filter users first by whether they're using the desired software
|
# filter users first by whether they're using the desired software
|
||||||
# this lets us send book updates only to other bw servers
|
# this lets us send book updates only to other bw servers
|
||||||
if software:
|
if software:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(bookwyrm_user=(software == "bookwyrm"))
|
||||||
bookwyrm_user=(software == 'bookwyrm')
|
|
||||||
)
|
|
||||||
# if there's a user, we only want to send to the user's followers
|
# if there's a user, we only want to send to the user's followers
|
||||||
if user:
|
if user:
|
||||||
queryset = queryset.filter(following=user)
|
queryset = queryset.filter(following=user)
|
||||||
|
|
||||||
# ideally, we will send to shared inboxes for efficiency
|
# ideally, we will send to shared inboxes for efficiency
|
||||||
shared_inboxes = queryset.filter(
|
shared_inboxes = (
|
||||||
shared_inbox__isnull=False
|
queryset.filter(shared_inbox__isnull=False)
|
||||||
).values_list('shared_inbox', flat=True).distinct()
|
.values_list("shared_inbox", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
# but not everyone has a shared inbox
|
# but not everyone has a shared inbox
|
||||||
inboxes = queryset.filter(
|
inboxes = queryset.filter(shared_inbox__isnull=True).values_list(
|
||||||
shared_inbox__isnull=True
|
"inbox", flat=True
|
||||||
).values_list('inbox', flat=True)
|
)
|
||||||
recipients += list(shared_inboxes) + list(inboxes)
|
recipients += list(shared_inboxes) + list(inboxes)
|
||||||
return recipients
|
return recipients
|
||||||
|
|
||||||
|
|
||||||
def to_activity_dataclass(self):
|
def to_activity_dataclass(self):
|
||||||
''' convert from a model to an activity '''
|
""" convert from a model to an activity """
|
||||||
activity = generate_activity(self)
|
activity = generate_activity(self)
|
||||||
return self.activity_serializer(**activity)
|
return self.activity_serializer(**activity)
|
||||||
|
|
||||||
def to_activity(self, **kwargs): # pylint: disable=unused-argument
|
def to_activity(self, **kwargs): # pylint: disable=unused-argument
|
||||||
''' convert from a model to a json activity '''
|
""" convert from a model to a json activity """
|
||||||
return self.to_activity_dataclass().serialize()
|
return self.to_activity_dataclass().serialize()
|
||||||
|
|
||||||
|
|
||||||
class ObjectMixin(ActivitypubMixin):
|
class ObjectMixin(ActivitypubMixin):
|
||||||
''' add this mixin for object models that are AP serializable '''
|
""" add this mixin for object models that are AP serializable """
|
||||||
|
|
||||||
def save(self, *args, created=None, **kwargs):
|
def save(self, *args, created=None, **kwargs):
|
||||||
''' broadcast created/updated/deleted objects as appropriate '''
|
""" broadcast created/updated/deleted objects as appropriate """
|
||||||
broadcast = kwargs.get('broadcast', True)
|
broadcast = kwargs.get("broadcast", True)
|
||||||
# this bonus kwarg woul cause an error in the base save method
|
# this bonus kwarg woul cause an error in the base save method
|
||||||
if 'broadcast' in kwargs:
|
if "broadcast" in kwargs:
|
||||||
del kwargs['broadcast']
|
del kwargs["broadcast"]
|
||||||
|
|
||||||
created = created or not bool(self.id)
|
created = created or not bool(self.id)
|
||||||
# first off, we want to save normally no matter what
|
# first off, we want to save normally no matter what
|
||||||
|
@ -183,7 +188,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||||
return
|
return
|
||||||
|
|
||||||
# this will work for objects owned by a user (lists, shelves)
|
# this will work for objects owned by a user (lists, shelves)
|
||||||
user = self.user if hasattr(self, 'user') else None
|
user = self.user if hasattr(self, "user") else None
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
# broadcast Create activities for objects owned by a local user
|
# broadcast Create activities for objects owned by a local user
|
||||||
|
@ -193,10 +198,10 @@ class ObjectMixin(ActivitypubMixin):
|
||||||
try:
|
try:
|
||||||
software = None
|
software = None
|
||||||
# do we have a "pure" activitypub version of this for mastodon?
|
# do we have a "pure" activitypub version of this for mastodon?
|
||||||
if hasattr(self, 'pure_content'):
|
if hasattr(self, "pure_content"):
|
||||||
pure_activity = self.to_create_activity(user, pure=True)
|
pure_activity = self.to_create_activity(user, pure=True)
|
||||||
self.broadcast(pure_activity, user, software='other')
|
self.broadcast(pure_activity, user, software="other")
|
||||||
software = 'bookwyrm'
|
software = "bookwyrm"
|
||||||
# sends to BW only if we just did a pure version for masto
|
# sends to BW only if we just did a pure version for masto
|
||||||
activity = self.to_create_activity(user)
|
activity = self.to_create_activity(user)
|
||||||
self.broadcast(activity, user, software=software)
|
self.broadcast(activity, user, software=software)
|
||||||
|
@ -209,39 +214,38 @@ class ObjectMixin(ActivitypubMixin):
|
||||||
# --- updating an existing object
|
# --- updating an existing object
|
||||||
if not user:
|
if not user:
|
||||||
# users don't have associated users, they ARE users
|
# users don't have associated users, they ARE users
|
||||||
user_model = apps.get_model('bookwyrm.User', require_ready=True)
|
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
||||||
if isinstance(self, user_model):
|
if isinstance(self, user_model):
|
||||||
user = self
|
user = self
|
||||||
# book data tracks last editor
|
# book data tracks last editor
|
||||||
elif hasattr(self, 'last_edited_by'):
|
elif hasattr(self, "last_edited_by"):
|
||||||
user = self.last_edited_by
|
user = self.last_edited_by
|
||||||
# again, if we don't know the user or they're remote, don't bother
|
# again, if we don't know the user or they're remote, don't bother
|
||||||
if not user or not user.local:
|
if not user or not user.local:
|
||||||
return
|
return
|
||||||
|
|
||||||
# is this a deletion?
|
# is this a deletion?
|
||||||
if hasattr(self, 'deleted') and self.deleted:
|
if hasattr(self, "deleted") and self.deleted:
|
||||||
activity = self.to_delete_activity(user)
|
activity = self.to_delete_activity(user)
|
||||||
else:
|
else:
|
||||||
activity = self.to_update_activity(user)
|
activity = self.to_update_activity(user)
|
||||||
self.broadcast(activity, user)
|
self.broadcast(activity, user)
|
||||||
|
|
||||||
|
|
||||||
def to_create_activity(self, user, **kwargs):
|
def to_create_activity(self, user, **kwargs):
|
||||||
''' returns the object wrapped in a Create activity '''
|
""" returns the object wrapped in a Create activity """
|
||||||
activity_object = self.to_activity_dataclass(**kwargs)
|
activity_object = self.to_activity_dataclass(**kwargs)
|
||||||
|
|
||||||
signature = None
|
signature = None
|
||||||
create_id = self.remote_id + '/activity'
|
create_id = self.remote_id + "/activity"
|
||||||
if hasattr(activity_object, 'content') and activity_object.content:
|
if hasattr(activity_object, "content") and activity_object.content:
|
||||||
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
|
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
|
||||||
content = activity_object.content
|
content = activity_object.content
|
||||||
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
|
signed_message = signer.sign(SHA256.new(content.encode("utf8")))
|
||||||
|
|
||||||
signature = activitypub.Signature(
|
signature = activitypub.Signature(
|
||||||
creator='%s#main-key' % user.remote_id,
|
creator="%s#main-key" % user.remote_id,
|
||||||
created=activity_object.published,
|
created=activity_object.published,
|
||||||
signatureValue=b64encode(signed_message).decode('utf8')
|
signatureValue=b64encode(signed_message).decode("utf8"),
|
||||||
)
|
)
|
||||||
|
|
||||||
return activitypub.Create(
|
return activitypub.Create(
|
||||||
|
@ -253,50 +257,48 @@ class ObjectMixin(ActivitypubMixin):
|
||||||
signature=signature,
|
signature=signature,
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
def to_delete_activity(self, user):
|
def to_delete_activity(self, user):
|
||||||
''' notice of deletion '''
|
""" notice of deletion """
|
||||||
return activitypub.Delete(
|
return activitypub.Delete(
|
||||||
id=self.remote_id + '/activity',
|
id=self.remote_id + "/activity",
|
||||||
actor=user.remote_id,
|
actor=user.remote_id,
|
||||||
to=['%s/followers' % user.remote_id],
|
to=["%s/followers" % user.remote_id],
|
||||||
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
cc=["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
object=self,
|
||||||
|
).serialize()
|
||||||
|
|
||||||
|
def to_update_activity(self, user):
|
||||||
|
""" wrapper for Updates to an activity """
|
||||||
|
activity_id = "%s#update/%s" % (self.remote_id, uuid4())
|
||||||
|
return activitypub.Update(
|
||||||
|
id=activity_id,
|
||||||
|
actor=user.remote_id,
|
||||||
|
to=["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
object=self,
|
object=self,
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
def to_update_activity(self, user):
|
|
||||||
''' wrapper for Updates to an activity '''
|
|
||||||
activity_id = '%s#update/%s' % (self.remote_id, uuid4())
|
|
||||||
return activitypub.Update(
|
|
||||||
id=activity_id,
|
|
||||||
actor=user.remote_id,
|
|
||||||
to=['https://www.w3.org/ns/activitystreams#Public'],
|
|
||||||
object=self
|
|
||||||
).serialize()
|
|
||||||
|
|
||||||
|
|
||||||
class OrderedCollectionPageMixin(ObjectMixin):
|
class OrderedCollectionPageMixin(ObjectMixin):
|
||||||
''' just the paginator utilities, so you don't HAVE to
|
"""just the paginator utilities, so you don't HAVE to
|
||||||
override ActivitypubMixin's to_activity (ie, for outbox) '''
|
override ActivitypubMixin's to_activity (ie, for outbox)"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collection_remote_id(self):
|
def collection_remote_id(self):
|
||||||
''' this can be overriden if there's a special remote id, ie outbox '''
|
""" this can be overriden if there's a special remote id, ie outbox """
|
||||||
return self.remote_id
|
return self.remote_id
|
||||||
|
|
||||||
|
def to_ordered_collection(
|
||||||
def to_ordered_collection(self, queryset, \
|
self, queryset, remote_id=None, page=False, collection_only=False, **kwargs
|
||||||
remote_id=None, page=False, collection_only=False, **kwargs):
|
):
|
||||||
''' an ordered collection of whatevers '''
|
""" an ordered collection of whatevers """
|
||||||
if not queryset.ordered:
|
if not queryset.ordered:
|
||||||
raise RuntimeError('queryset must be ordered')
|
raise RuntimeError("queryset must be ordered")
|
||||||
|
|
||||||
remote_id = remote_id or self.remote_id
|
remote_id = remote_id or self.remote_id
|
||||||
if page:
|
if page:
|
||||||
return to_ordered_collection_page(
|
return to_ordered_collection_page(queryset, remote_id, **kwargs)
|
||||||
queryset, remote_id, **kwargs)
|
|
||||||
|
|
||||||
if collection_only or not hasattr(self, 'activity_serializer'):
|
if collection_only or not hasattr(self, "activity_serializer"):
|
||||||
serializer = activitypub.OrderedCollection
|
serializer = activitypub.OrderedCollection
|
||||||
activity = {}
|
activity = {}
|
||||||
else:
|
else:
|
||||||
|
@ -305,23 +307,24 @@ class OrderedCollectionPageMixin(ObjectMixin):
|
||||||
activity = generate_activity(self)
|
activity = generate_activity(self)
|
||||||
|
|
||||||
if remote_id:
|
if remote_id:
|
||||||
activity['id'] = remote_id
|
activity["id"] = remote_id
|
||||||
|
|
||||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||||
# add computed fields specific to orderd collections
|
# add computed fields specific to orderd collections
|
||||||
activity['totalItems'] = paginated.count
|
activity["totalItems"] = paginated.count
|
||||||
activity['first'] = '%s?page=1' % remote_id
|
activity["first"] = "%s?page=1" % remote_id
|
||||||
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
|
activity["last"] = "%s?page=%d" % (remote_id, paginated.num_pages)
|
||||||
|
|
||||||
return serializer(**activity)
|
return serializer(**activity)
|
||||||
|
|
||||||
|
|
||||||
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||||
''' extends activitypub models to work as ordered collections '''
|
""" extends activitypub models to work as ordered collections """
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collection_queryset(self):
|
def collection_queryset(self):
|
||||||
''' usually an ordered collection model aggregates a different model '''
|
""" usually an ordered collection model aggregates a different model """
|
||||||
raise NotImplementedError('Model must define collection_queryset')
|
raise NotImplementedError("Model must define collection_queryset")
|
||||||
|
|
||||||
activity_serializer = activitypub.OrderedCollection
|
activity_serializer = activitypub.OrderedCollection
|
||||||
|
|
||||||
|
@ -329,18 +332,20 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||||
|
|
||||||
def to_activity(self, **kwargs):
|
def to_activity(self, **kwargs):
|
||||||
''' an ordered collection of the specified model queryset '''
|
""" an ordered collection of the specified model queryset """
|
||||||
return self.to_ordered_collection(
|
return self.to_ordered_collection(
|
||||||
self.collection_queryset, **kwargs).serialize()
|
self.collection_queryset, **kwargs
|
||||||
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
class CollectionItemMixin(ActivitypubMixin):
|
class CollectionItemMixin(ActivitypubMixin):
|
||||||
''' for items that are part of an (Ordered)Collection '''
|
""" for items that are part of an (Ordered)Collection """
|
||||||
|
|
||||||
activity_serializer = activitypub.Add
|
activity_serializer = activitypub.Add
|
||||||
object_field = collection_field = None
|
object_field = collection_field = None
|
||||||
|
|
||||||
def save(self, *args, broadcast=True, **kwargs):
|
def save(self, *args, broadcast=True, **kwargs):
|
||||||
''' broadcast updated '''
|
""" broadcast updated """
|
||||||
created = not bool(self.id)
|
created = not bool(self.id)
|
||||||
# first off, we want to save normally no matter what
|
# first off, we want to save normally no matter what
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
@ -353,89 +358,91 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||||
activity = self.to_add_activity()
|
activity = self.to_add_activity()
|
||||||
self.broadcast(activity, self.user)
|
self.broadcast(activity, self.user)
|
||||||
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
''' broadcast a remove activity '''
|
""" broadcast a remove activity """
|
||||||
activity = self.to_remove_activity()
|
activity = self.to_remove_activity()
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
self.broadcast(activity, self.user)
|
self.broadcast(activity, self.user)
|
||||||
|
|
||||||
|
|
||||||
def to_add_activity(self):
|
def to_add_activity(self):
|
||||||
''' AP for shelving a book'''
|
""" AP for shelving a book"""
|
||||||
object_field = getattr(self, self.object_field)
|
object_field = getattr(self, self.object_field)
|
||||||
collection_field = getattr(self, self.collection_field)
|
collection_field = getattr(self, self.collection_field)
|
||||||
return activitypub.Add(
|
return activitypub.Add(
|
||||||
id='%s#add' % self.remote_id,
|
id="%s#add" % self.remote_id,
|
||||||
actor=self.user.remote_id,
|
actor=self.user.remote_id,
|
||||||
object=object_field,
|
object=object_field,
|
||||||
target=collection_field.remote_id
|
target=collection_field.remote_id,
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
def to_remove_activity(self):
|
def to_remove_activity(self):
|
||||||
''' AP for un-shelving a book'''
|
""" AP for un-shelving a book"""
|
||||||
object_field = getattr(self, self.object_field)
|
object_field = getattr(self, self.object_field)
|
||||||
collection_field = getattr(self, self.collection_field)
|
collection_field = getattr(self, self.collection_field)
|
||||||
return activitypub.Remove(
|
return activitypub.Remove(
|
||||||
id='%s#remove' % self.remote_id,
|
id="%s#remove" % self.remote_id,
|
||||||
actor=self.user.remote_id,
|
actor=self.user.remote_id,
|
||||||
object=object_field,
|
object=object_field,
|
||||||
target=collection_field.remote_id
|
target=collection_field.remote_id,
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
class ActivityMixin(ActivitypubMixin):
|
class ActivityMixin(ActivitypubMixin):
|
||||||
''' add this mixin for models that are AP serializable '''
|
""" add this mixin for models that are AP serializable """
|
||||||
|
|
||||||
def save(self, *args, broadcast=True, **kwargs):
|
def save(self, *args, broadcast=True, **kwargs):
|
||||||
''' broadcast activity '''
|
""" broadcast activity """
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
user = self.user if hasattr(self, 'user') else self.user_subject
|
user = self.user if hasattr(self, "user") else self.user_subject
|
||||||
if broadcast and user.local:
|
if broadcast and user.local:
|
||||||
self.broadcast(self.to_activity(), user)
|
self.broadcast(self.to_activity(), user)
|
||||||
|
|
||||||
|
|
||||||
def delete(self, *args, broadcast=True, **kwargs):
|
def delete(self, *args, broadcast=True, **kwargs):
|
||||||
''' nevermind, undo that activity '''
|
""" nevermind, undo that activity """
|
||||||
user = self.user if hasattr(self, 'user') else self.user_subject
|
user = self.user if hasattr(self, "user") else self.user_subject
|
||||||
if broadcast and user.local:
|
if broadcast and user.local:
|
||||||
self.broadcast(self.to_undo_activity(), user)
|
self.broadcast(self.to_undo_activity(), user)
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def to_undo_activity(self):
|
def to_undo_activity(self):
|
||||||
''' undo an action '''
|
""" undo an action """
|
||||||
user = self.user if hasattr(self, 'user') else self.user_subject
|
user = self.user if hasattr(self, "user") else self.user_subject
|
||||||
return activitypub.Undo(
|
return activitypub.Undo(
|
||||||
id='%s#undo' % self.remote_id,
|
id="%s#undo" % self.remote_id,
|
||||||
actor=user.remote_id,
|
actor=user.remote_id,
|
||||||
object=self,
|
object=self,
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
def generate_activity(obj):
|
def generate_activity(obj):
|
||||||
''' go through the fields on an object '''
|
""" go through the fields on an object """
|
||||||
activity = {}
|
activity = {}
|
||||||
for field in obj.activity_fields:
|
for field in obj.activity_fields:
|
||||||
field.set_activity_from_field(activity, obj)
|
field.set_activity_from_field(activity, obj)
|
||||||
|
|
||||||
if hasattr(obj, 'serialize_reverse_fields'):
|
if hasattr(obj, "serialize_reverse_fields"):
|
||||||
# for example, editions of a work
|
# for example, editions of a work
|
||||||
for model_field_name, activity_field_name, sort_field in \
|
for (
|
||||||
obj.serialize_reverse_fields:
|
model_field_name,
|
||||||
|
activity_field_name,
|
||||||
|
sort_field,
|
||||||
|
) in obj.serialize_reverse_fields:
|
||||||
related_field = getattr(obj, model_field_name)
|
related_field = getattr(obj, model_field_name)
|
||||||
activity[activity_field_name] = \
|
activity[activity_field_name] = unfurl_related_field(
|
||||||
unfurl_related_field(related_field, sort_field)
|
related_field, sort_field
|
||||||
|
)
|
||||||
|
|
||||||
if not activity.get('id'):
|
if not activity.get("id"):
|
||||||
activity['id'] = obj.get_remote_id()
|
activity["id"] = obj.get_remote_id()
|
||||||
return activity
|
return activity
|
||||||
|
|
||||||
|
|
||||||
def unfurl_related_field(related_field, sort_field=None):
|
def unfurl_related_field(related_field, sort_field=None):
|
||||||
''' load reverse lookups (like public key owner or Status attachment '''
|
""" load reverse lookups (like public key owner or Status attachment """
|
||||||
if hasattr(related_field, 'all'):
|
if hasattr(related_field, "all"):
|
||||||
return [unfurl_related_field(i) for i in related_field.order_by(
|
return [
|
||||||
sort_field).all()]
|
unfurl_related_field(i) for i in related_field.order_by(sort_field).all()
|
||||||
|
]
|
||||||
if related_field.reverse_unfurl:
|
if related_field.reverse_unfurl:
|
||||||
return related_field.field_to_activity()
|
return related_field.field_to_activity()
|
||||||
return related_field.remote_id
|
return related_field.remote_id
|
||||||
|
@ -443,23 +450,23 @@ def unfurl_related_field(related_field, sort_field=None):
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def broadcast_task(sender_id, activity, recipients):
|
def broadcast_task(sender_id, activity, recipients):
|
||||||
''' the celery task for broadcast '''
|
""" the celery task for broadcast """
|
||||||
user_model = apps.get_model('bookwyrm.User', require_ready=True)
|
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
||||||
sender = user_model.objects.get(id=sender_id)
|
sender = user_model.objects.get(id=sender_id)
|
||||||
for recipient in recipients:
|
for recipient in recipients:
|
||||||
try:
|
try:
|
||||||
sign_and_send(sender, activity, recipient)
|
sign_and_send(sender, activity, recipient)
|
||||||
except (HTTPError, SSLError) as e:
|
except (HTTPError, SSLError, ConnectionError) as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
|
|
||||||
def sign_and_send(sender, data, destination):
|
def sign_and_send(sender, data, destination):
|
||||||
''' crpyto whatever and http junk '''
|
""" crpyto whatever and http junk """
|
||||||
now = http_date()
|
now = http_date()
|
||||||
|
|
||||||
if not sender.key_pair.private_key:
|
if not sender.key_pair.private_key:
|
||||||
# this shouldn't happen. it would be bad if it happened.
|
# this shouldn't happen. it would be bad if it happened.
|
||||||
raise ValueError('No private key found for sender')
|
raise ValueError("No private key found for sender")
|
||||||
|
|
||||||
digest = make_digest(data)
|
digest = make_digest(data)
|
||||||
|
|
||||||
|
@ -467,11 +474,11 @@ def sign_and_send(sender, data, destination):
|
||||||
destination,
|
destination,
|
||||||
data=data,
|
data=data,
|
||||||
headers={
|
headers={
|
||||||
'Date': now,
|
"Date": now,
|
||||||
'Digest': digest,
|
"Digest": digest,
|
||||||
'Signature': make_signature(sender, destination, now, digest),
|
"Signature": make_signature(sender, destination, now, digest),
|
||||||
'Content-Type': 'application/activity+json; charset=utf-8',
|
"Content-Type": "application/activity+json; charset=utf-8",
|
||||||
'User-Agent': USER_AGENT,
|
"User-Agent": USER_AGENT,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
|
@ -481,8 +488,9 @@ def sign_and_send(sender, data, destination):
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def to_ordered_collection_page(
|
def to_ordered_collection_page(
|
||||||
queryset, remote_id, id_only=False, page=1, pure=False, **kwargs):
|
queryset, remote_id, id_only=False, page=1, pure=False, **kwargs
|
||||||
''' serialize and pagiante a queryset '''
|
):
|
||||||
|
""" serialize and pagiante a queryset """
|
||||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||||
|
|
||||||
activity_page = paginated.page(page)
|
activity_page = paginated.page(page)
|
||||||
|
@ -493,14 +501,13 @@ def to_ordered_collection_page(
|
||||||
|
|
||||||
prev_page = next_page = None
|
prev_page = next_page = None
|
||||||
if activity_page.has_next():
|
if activity_page.has_next():
|
||||||
next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
|
next_page = "%s?page=%d" % (remote_id, activity_page.next_page_number())
|
||||||
if activity_page.has_previous():
|
if activity_page.has_previous():
|
||||||
prev_page = '%s?page=%d' % \
|
prev_page = "%s?page=%d" % (remote_id, activity_page.previous_page_number())
|
||||||
(remote_id, activity_page.previous_page_number())
|
|
||||||
return activitypub.OrderedCollectionPage(
|
return activitypub.OrderedCollectionPage(
|
||||||
id='%s?page=%s' % (remote_id, page),
|
id="%s?page=%s" % (remote_id, page),
|
||||||
partOf=remote_id,
|
partOf=remote_id,
|
||||||
orderedItems=items,
|
orderedItems=items,
|
||||||
next=next_page,
|
next=next_page,
|
||||||
prev=prev_page
|
prev=prev_page,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' media that is posted in the app '''
|
""" media that is posted in the app """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
|
@ -8,23 +8,25 @@ from . import fields
|
||||||
|
|
||||||
|
|
||||||
class Attachment(ActivitypubMixin, BookWyrmModel):
|
class Attachment(ActivitypubMixin, BookWyrmModel):
|
||||||
''' an image (or, in the future, video etc) associated with a status '''
|
""" an image (or, in the future, video etc) associated with a status """
|
||||||
|
|
||||||
status = models.ForeignKey(
|
status = models.ForeignKey(
|
||||||
'Status',
|
"Status", on_delete=models.CASCADE, related_name="attachments", null=True
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='attachments',
|
|
||||||
null=True
|
|
||||||
)
|
)
|
||||||
reverse_unfurl = True
|
reverse_unfurl = True
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' one day we'll have other types of attachments besides images '''
|
""" one day we'll have other types of attachments besides images """
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Image(Attachment):
|
class Image(Attachment):
|
||||||
''' an image attachment '''
|
""" an image attachment """
|
||||||
|
|
||||||
image = fields.ImageField(
|
image = fields.ImageField(
|
||||||
upload_to='status/', null=True, blank=True, activitypub_field='url')
|
upload_to="status/", null=True, blank=True, activitypub_field="url"
|
||||||
caption = fields.TextField(null=True, blank=True, activitypub_field='name')
|
)
|
||||||
|
caption = fields.TextField(null=True, blank=True, activitypub_field="name")
|
||||||
|
|
||||||
activity_serializer = activitypub.Image
|
activity_serializer = activitypub.Image
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' database schema for info about authors '''
|
""" database schema for info about authors """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
|
@ -9,9 +9,11 @@ from . import fields
|
||||||
|
|
||||||
|
|
||||||
class Author(BookDataModel):
|
class Author(BookDataModel):
|
||||||
''' basic biographic info '''
|
""" basic biographic info """
|
||||||
|
|
||||||
wikipedia_link = fields.CharField(
|
wikipedia_link = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
# idk probably other keys would be useful here?
|
# idk probably other keys would be useful here?
|
||||||
born = fields.DateTimeField(blank=True, null=True)
|
born = fields.DateTimeField(blank=True, null=True)
|
||||||
died = fields.DateTimeField(blank=True, null=True)
|
died = fields.DateTimeField(blank=True, null=True)
|
||||||
|
@ -22,7 +24,7 @@ class Author(BookDataModel):
|
||||||
bio = fields.HtmlField(null=True, blank=True)
|
bio = fields.HtmlField(null=True, blank=True)
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
''' editions and works both use "book" instead of model_name '''
|
""" editions and works both use "book" instead of model_name """
|
||||||
return 'https://%s/author/%s' % (DOMAIN, self.id)
|
return "https://%s/author/%s" % (DOMAIN, self.id)
|
||||||
|
|
||||||
activity_serializer = activitypub.Author
|
activity_serializer = activitypub.Author
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' base model with default fields '''
|
""" base model with default fields """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
@ -7,34 +7,36 @@ from .fields import RemoteIdField
|
||||||
|
|
||||||
|
|
||||||
class BookWyrmModel(models.Model):
|
class BookWyrmModel(models.Model):
|
||||||
''' shared fields '''
|
""" shared fields """
|
||||||
|
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
remote_id = RemoteIdField(null=True, activitypub_field='id')
|
remote_id = RemoteIdField(null=True, activitypub_field="id")
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
''' generate a url that resolves to the local object '''
|
""" generate a url that resolves to the local object """
|
||||||
base_path = 'https://%s' % DOMAIN
|
base_path = "https://%s" % DOMAIN
|
||||||
if hasattr(self, 'user'):
|
if hasattr(self, "user"):
|
||||||
base_path = '%s%s' % (base_path, self.user.local_path)
|
base_path = "%s%s" % (base_path, self.user.local_path)
|
||||||
model_name = type(self).__name__.lower()
|
model_name = type(self).__name__.lower()
|
||||||
return '%s/%s/%d' % (base_path, model_name, self.id)
|
return "%s/%s/%d" % (base_path, model_name, self.id)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' this is just here to provide default fields for other models '''
|
""" this is just here to provide default fields for other models """
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local_path(self):
|
def local_path(self):
|
||||||
''' how to link to this object in the local app '''
|
""" how to link to this object in the local app """
|
||||||
return self.get_remote_id().replace('https://%s' % DOMAIN, '')
|
return self.get_remote_id().replace("https://%s" % DOMAIN, "")
|
||||||
|
|
||||||
|
|
||||||
@receiver(models.signals.post_save)
|
@receiver(models.signals.post_save)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def execute_after_save(sender, instance, created, *args, **kwargs):
|
def execute_after_save(sender, instance, created, *args, **kwargs):
|
||||||
''' set the remote_id after save (when the id is available) '''
|
""" set the remote_id after save (when the id is available) """
|
||||||
if not created or not hasattr(instance, 'get_remote_id'):
|
if not created or not hasattr(instance, "get_remote_id"):
|
||||||
return
|
return
|
||||||
if not instance.remote_id:
|
if not instance.remote_id:
|
||||||
instance.remote_id = instance.get_remote_id()
|
instance.remote_id = instance.get_remote_id()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' database schema for books and shelves '''
|
""" database schema for books and shelves """
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -11,25 +11,30 @@ from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|
||||||
class BookDataModel(ObjectMixin, BookWyrmModel):
|
class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||||
''' fields shared between editable book data (books, works, authors) '''
|
""" fields shared between editable book data (books, works, authors) """
|
||||||
|
|
||||||
origin_id = models.CharField(max_length=255, null=True, blank=True)
|
origin_id = models.CharField(max_length=255, null=True, blank=True)
|
||||||
openlibrary_key = fields.CharField(
|
openlibrary_key = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
librarything_key = fields.CharField(
|
librarything_key = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
goodreads_key = fields.CharField(
|
goodreads_key = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
|
|
||||||
last_edited_by = models.ForeignKey(
|
last_edited_by = models.ForeignKey("User", on_delete=models.PROTECT, null=True)
|
||||||
'User', on_delete=models.PROTECT, null=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' can't initialize this model, that wouldn't make sense '''
|
""" can't initialize this model, that wouldn't make sense """
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' ensure that the remote_id is within this instance '''
|
""" ensure that the remote_id is within this instance """
|
||||||
if self.id:
|
if self.id:
|
||||||
self.remote_id = self.get_remote_id()
|
self.remote_id = self.get_remote_id()
|
||||||
else:
|
else:
|
||||||
|
@ -37,11 +42,15 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||||
self.remote_id = None
|
self.remote_id = None
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def broadcast(self, activity, sender, software="bookwyrm"):
|
||||||
|
""" only send book data updates to other bookwyrm instances """
|
||||||
|
super().broadcast(activity, sender, software=software)
|
||||||
|
|
||||||
|
|
||||||
class Book(BookDataModel):
|
class Book(BookDataModel):
|
||||||
''' a generic book, which can mean either an edition or a work '''
|
""" a generic book, which can mean either an edition or a work """
|
||||||
connector = models.ForeignKey(
|
|
||||||
'Connector', on_delete=models.PROTECT, null=True)
|
connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
|
||||||
|
|
||||||
# book/work metadata
|
# book/work metadata
|
||||||
title = fields.CharField(max_length=255)
|
title = fields.CharField(max_length=255)
|
||||||
|
@ -59,9 +68,10 @@ class Book(BookDataModel):
|
||||||
subject_places = fields.ArrayField(
|
subject_places = fields.ArrayField(
|
||||||
models.CharField(max_length=255), blank=True, null=True, default=list
|
models.CharField(max_length=255), blank=True, null=True, default=list
|
||||||
)
|
)
|
||||||
authors = fields.ManyToManyField('Author')
|
authors = fields.ManyToManyField("Author")
|
||||||
cover = fields.ImageField(
|
cover = fields.ImageField(
|
||||||
upload_to='covers/', blank=True, null=True, alt_field='alt_text')
|
upload_to="covers/", blank=True, null=True, alt_field="alt_text"
|
||||||
|
)
|
||||||
first_published_date = fields.DateTimeField(blank=True, null=True)
|
first_published_date = fields.DateTimeField(blank=True, null=True)
|
||||||
published_date = fields.DateTimeField(blank=True, null=True)
|
published_date = fields.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
@ -69,42 +79,43 @@ class Book(BookDataModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def author_text(self):
|
def author_text(self):
|
||||||
''' format a list of authors '''
|
""" format a list of authors """
|
||||||
return ', '.join(a.name for a in self.authors.all())
|
return ", ".join(a.name for a in self.authors.all())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_readthrough(self):
|
def latest_readthrough(self):
|
||||||
''' most recent readthrough activity '''
|
""" most recent readthrough activity """
|
||||||
return self.readthrough_set.order_by('-updated_date').first()
|
return self.readthrough_set.order_by("-updated_date").first()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def edition_info(self):
|
def edition_info(self):
|
||||||
''' properties of this edition, as a string '''
|
""" properties of this edition, as a string """
|
||||||
items = [
|
items = [
|
||||||
self.physical_format if hasattr(self, 'physical_format') else None,
|
self.physical_format if hasattr(self, "physical_format") else None,
|
||||||
self.languages[0] + ' language' if self.languages and \
|
self.languages[0] + " language"
|
||||||
self.languages[0] != 'English' else None,
|
if self.languages and self.languages[0] != "English"
|
||||||
|
else None,
|
||||||
str(self.published_date.year) if self.published_date else None,
|
str(self.published_date.year) if self.published_date else None,
|
||||||
]
|
]
|
||||||
return ', '.join(i for i in items if i)
|
return ", ".join(i for i in items if i)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alt_text(self):
|
def alt_text(self):
|
||||||
''' image alt test '''
|
""" image alt test """
|
||||||
text = '%s cover' % self.title
|
text = "%s" % self.title
|
||||||
if self.edition_info:
|
if self.edition_info:
|
||||||
text += ' (%s)' % self.edition_info
|
text += " (%s)" % self.edition_info
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' can't be abstract for query reasons, but you shouldn't USE it '''
|
""" can't be abstract for query reasons, but you shouldn't USE it """
|
||||||
if not isinstance(self, Edition) and not isinstance(self, Work):
|
if not isinstance(self, Edition) and not isinstance(self, Work):
|
||||||
raise ValueError('Books should be added as Editions or Works')
|
raise ValueError("Books should be added as Editions or Works")
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
''' editions and works both use "book" instead of model_name '''
|
""" editions and works both use "book" instead of model_name """
|
||||||
return 'https://%s/book/%d' % (DOMAIN, self.id)
|
return "https://%s/book/%d" % (DOMAIN, self.id)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{} key={!r} title={!r}>".format(
|
return "<{} key={!r} title={!r}>".format(
|
||||||
|
@ -115,76 +126,82 @@ class Book(BookDataModel):
|
||||||
|
|
||||||
|
|
||||||
class Work(OrderedCollectionPageMixin, Book):
|
class Work(OrderedCollectionPageMixin, Book):
|
||||||
''' a work (an abstract concept of a book that manifests in an edition) '''
|
""" a work (an abstract concept of a book that manifests in an edition) """
|
||||||
|
|
||||||
# library of congress catalog control number
|
# library of congress catalog control number
|
||||||
lccn = fields.CharField(
|
lccn = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
# this has to be nullable but should never be null
|
# this has to be nullable but should never be null
|
||||||
default_edition = fields.ForeignKey(
|
default_edition = fields.ForeignKey(
|
||||||
'Edition',
|
"Edition", on_delete=models.PROTECT, null=True, load_remote=False
|
||||||
on_delete=models.PROTECT,
|
|
||||||
null=True,
|
|
||||||
load_remote=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' set some fields on the edition object '''
|
""" set some fields on the edition object """
|
||||||
# set rank
|
# set rank
|
||||||
for edition in self.editions.all():
|
for edition in self.editions.all():
|
||||||
edition.save()
|
edition.save()
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_default_edition(self):
|
def get_default_edition(self):
|
||||||
''' in case the default edition is not set '''
|
""" in case the default edition is not set """
|
||||||
return self.default_edition or self.editions.order_by(
|
return self.default_edition or self.editions.order_by("-edition_rank").first()
|
||||||
'-edition_rank'
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def to_edition_list(self, **kwargs):
|
def to_edition_list(self, **kwargs):
|
||||||
''' an ordered collection of editions '''
|
""" an ordered collection of editions """
|
||||||
return self.to_ordered_collection(
|
return self.to_ordered_collection(
|
||||||
self.editions.order_by('-edition_rank').all(),
|
self.editions.order_by("-edition_rank").all(),
|
||||||
remote_id='%s/editions' % self.remote_id,
|
remote_id="%s/editions" % self.remote_id,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_serializer = activitypub.Work
|
activity_serializer = activitypub.Work
|
||||||
serialize_reverse_fields = [('editions', 'editions', '-edition_rank')]
|
serialize_reverse_fields = [("editions", "editions", "-edition_rank")]
|
||||||
deserialize_reverse_fields = [('editions', 'editions')]
|
deserialize_reverse_fields = [("editions", "editions")]
|
||||||
|
|
||||||
|
|
||||||
class Edition(Book):
|
class Edition(Book):
|
||||||
''' an edition of a book '''
|
""" an edition of a book """
|
||||||
|
|
||||||
# these identifiers only apply to editions, not works
|
# these identifiers only apply to editions, not works
|
||||||
isbn_10 = fields.CharField(
|
isbn_10 = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
isbn_13 = fields.CharField(
|
isbn_13 = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
oclc_number = fields.CharField(
|
oclc_number = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
asin = fields.CharField(
|
asin = fields.CharField(
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True)
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
|
)
|
||||||
pages = fields.IntegerField(blank=True, null=True)
|
pages = fields.IntegerField(blank=True, null=True)
|
||||||
physical_format = fields.CharField(max_length=255, blank=True, null=True)
|
physical_format = fields.CharField(max_length=255, blank=True, null=True)
|
||||||
publishers = fields.ArrayField(
|
publishers = fields.ArrayField(
|
||||||
models.CharField(max_length=255), blank=True, default=list
|
models.CharField(max_length=255), blank=True, default=list
|
||||||
)
|
)
|
||||||
shelves = models.ManyToManyField(
|
shelves = models.ManyToManyField(
|
||||||
'Shelf',
|
"Shelf",
|
||||||
symmetrical=False,
|
symmetrical=False,
|
||||||
through='ShelfBook',
|
through="ShelfBook",
|
||||||
through_fields=('book', 'shelf')
|
through_fields=("book", "shelf"),
|
||||||
)
|
)
|
||||||
parent_work = fields.ForeignKey(
|
parent_work = fields.ForeignKey(
|
||||||
'Work', on_delete=models.PROTECT, null=True,
|
"Work",
|
||||||
related_name='editions', activitypub_field='work')
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
related_name="editions",
|
||||||
|
activitypub_field="work",
|
||||||
|
)
|
||||||
edition_rank = fields.IntegerField(default=0)
|
edition_rank = fields.IntegerField(default=0)
|
||||||
|
|
||||||
activity_serializer = activitypub.Edition
|
activity_serializer = activitypub.Edition
|
||||||
name_field = 'title'
|
name_field = "title"
|
||||||
|
|
||||||
def get_rank(self):
|
def get_rank(self):
|
||||||
''' calculate how complete the data is on this edition '''
|
""" calculate how complete the data is on this edition """
|
||||||
if self.parent_work and self.parent_work.default_edition == self:
|
if self.parent_work and self.parent_work.default_edition == self:
|
||||||
# default edition has the highest rank
|
# default edition has the highest rank
|
||||||
return 20
|
return 20
|
||||||
|
@ -200,9 +217,9 @@ class Edition(Book):
|
||||||
return rank
|
return rank
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' set some fields on the edition object '''
|
""" set some fields on the edition object """
|
||||||
# calculate isbn 10/13
|
# calculate isbn 10/13
|
||||||
if self.isbn_13 and self.isbn_13[:3] == '978' and not self.isbn_10:
|
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)
|
self.isbn_10 = isbn_13_to_10(self.isbn_13)
|
||||||
if self.isbn_10 and not self.isbn_13:
|
if self.isbn_10 and not self.isbn_13:
|
||||||
self.isbn_13 = isbn_10_to_13(self.isbn_10)
|
self.isbn_13 = isbn_10_to_13(self.isbn_10)
|
||||||
|
@ -214,17 +231,18 @@ class Edition(Book):
|
||||||
|
|
||||||
|
|
||||||
def isbn_10_to_13(isbn_10):
|
def isbn_10_to_13(isbn_10):
|
||||||
''' convert an isbn 10 into an isbn 13 '''
|
""" convert an isbn 10 into an isbn 13 """
|
||||||
isbn_10 = re.sub(r'[^0-9X]', '', isbn_10)
|
isbn_10 = re.sub(r"[^0-9X]", "", isbn_10)
|
||||||
# drop the last character of the isbn 10 number (the original checkdigit)
|
# drop the last character of the isbn 10 number (the original checkdigit)
|
||||||
converted = isbn_10[:9]
|
converted = isbn_10[:9]
|
||||||
# add "978" to the front
|
# add "978" to the front
|
||||||
converted = '978' + converted
|
converted = "978" + converted
|
||||||
# add a check digit to the end
|
# add a check digit to the end
|
||||||
# multiply the odd digits by 1 and the even digits by 3 and sum them
|
# multiply the odd digits by 1 and the even digits by 3 and sum them
|
||||||
try:
|
try:
|
||||||
checksum = sum(int(i) for i in converted[::2]) + \
|
checksum = sum(int(i) for i in converted[::2]) + sum(
|
||||||
sum(int(i) * 3 for i in converted[1::2])
|
int(i) * 3 for i in converted[1::2]
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
# add the checksum mod 10 to the end
|
# add the checksum mod 10 to the end
|
||||||
|
@ -235,11 +253,11 @@ def isbn_10_to_13(isbn_10):
|
||||||
|
|
||||||
|
|
||||||
def isbn_13_to_10(isbn_13):
|
def isbn_13_to_10(isbn_13):
|
||||||
''' convert isbn 13 to 10, if possible '''
|
""" convert isbn 13 to 10, if possible """
|
||||||
if isbn_13[:3] != '978':
|
if isbn_13[:3] != "978":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
isbn_13 = re.sub(r'[^0-9X]', '', isbn_13)
|
isbn_13 = re.sub(r"[^0-9X]", "", isbn_13)
|
||||||
|
|
||||||
# remove '978' and old checkdigit
|
# remove '978' and old checkdigit
|
||||||
converted = isbn_13[3:-1]
|
converted = isbn_13[3:-1]
|
||||||
|
@ -252,5 +270,5 @@ def isbn_13_to_10(isbn_13):
|
||||||
checkdigit = checksum % 11
|
checkdigit = checksum % 11
|
||||||
checkdigit = 11 - checkdigit
|
checkdigit = 11 - checkdigit
|
||||||
if checkdigit == 10:
|
if checkdigit == 10:
|
||||||
checkdigit = 'X'
|
checkdigit = "X"
|
||||||
return converted + str(checkdigit)
|
return converted + str(checkdigit)
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
''' manages interfaces with external sources of book data '''
|
""" manages interfaces with external sources of book data """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from bookwyrm.connectors.settings import CONNECTORS
|
from bookwyrm.connectors.settings import CONNECTORS
|
||||||
|
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
ConnectorFiles = models.TextChoices('ConnectorFiles', CONNECTORS)
|
ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
|
||||||
|
|
||||||
|
|
||||||
class Connector(BookWyrmModel):
|
class Connector(BookWyrmModel):
|
||||||
''' book data source connectors '''
|
""" book data source connectors """
|
||||||
|
|
||||||
identifier = models.CharField(max_length=255, unique=True)
|
identifier = models.CharField(max_length=255, unique=True)
|
||||||
priority = models.IntegerField(default=2)
|
priority = models.IntegerField(default=2)
|
||||||
name = models.CharField(max_length=255, null=True, blank=True)
|
name = models.CharField(max_length=255, null=True, blank=True)
|
||||||
local = models.BooleanField(default=False)
|
local = models.BooleanField(default=False)
|
||||||
connector_file = models.CharField(
|
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
||||||
max_length=255,
|
|
||||||
choices=ConnectorFiles.choices
|
|
||||||
)
|
|
||||||
api_key = models.CharField(max_length=255, null=True, blank=True)
|
api_key = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
base_url = models.CharField(max_length=255)
|
base_url = models.CharField(max_length=255)
|
||||||
books_url = models.CharField(max_length=255)
|
books_url = models.CharField(max_length=255)
|
||||||
covers_url = models.CharField(max_length=255)
|
covers_url = models.CharField(max_length=255)
|
||||||
search_url = models.CharField(max_length=255, null=True, blank=True)
|
search_url = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
politeness_delay = models.IntegerField(null=True, blank=True) # seconds
|
politeness_delay = models.IntegerField(null=True, blank=True) # seconds
|
||||||
max_query_count = models.IntegerField(null=True, blank=True)
|
max_query_count = models.IntegerField(null=True, blank=True)
|
||||||
|
@ -31,11 +32,12 @@ class Connector(BookWyrmModel):
|
||||||
query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True)
|
query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' check that there's code to actually use this connector '''
|
""" check that there's code to actually use this connector """
|
||||||
|
|
||||||
constraints = [
|
constraints = [
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
check=models.Q(connector_file__in=ConnectorFiles),
|
check=models.Q(connector_file__in=ConnectorFiles),
|
||||||
name='connector_file_valid'
|
name="connector_file_valid",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' like/fav/star a status '''
|
""" like/fav/star a status """
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -7,46 +7,61 @@ from bookwyrm import activitypub
|
||||||
from .activitypub_mixin import ActivityMixin
|
from .activitypub_mixin import ActivityMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
from .status import Status
|
||||||
|
|
||||||
|
|
||||||
class Favorite(ActivityMixin, BookWyrmModel):
|
class Favorite(ActivityMixin, BookWyrmModel):
|
||||||
''' fav'ing a post '''
|
""" fav'ing a post """
|
||||||
|
|
||||||
user = fields.ForeignKey(
|
user = fields.ForeignKey(
|
||||||
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
||||||
|
)
|
||||||
status = fields.ForeignKey(
|
status = fields.ForeignKey(
|
||||||
'Status', on_delete=models.PROTECT, activitypub_field='object')
|
"Status", on_delete=models.PROTECT, activitypub_field="object"
|
||||||
|
)
|
||||||
|
|
||||||
activity_serializer = activitypub.Like
|
activity_serializer = activitypub.Like
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ignore_activity(cls, activity):
|
||||||
|
""" don't bother with incoming favs of unknown statuses """
|
||||||
|
return not Status.objects.filter(remote_id=activity.object).exists()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' update user active time '''
|
""" update user active time """
|
||||||
self.user.last_active_date = timezone.now()
|
self.user.last_active_date = timezone.now()
|
||||||
self.user.save(broadcast=False)
|
self.user.save(broadcast=False)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if self.status.user.local and self.status.user != self.user:
|
if self.status.user.local and self.status.user != self.user:
|
||||||
notification_model = apps.get_model(
|
notification_model = apps.get_model(
|
||||||
'bookwyrm.Notification', require_ready=True)
|
"bookwyrm.Notification", require_ready=True
|
||||||
|
)
|
||||||
notification_model.objects.create(
|
notification_model.objects.create(
|
||||||
user=self.status.user,
|
user=self.status.user,
|
||||||
notification_type='FAVORITE',
|
notification_type="FAVORITE",
|
||||||
related_user=self.user,
|
related_user=self.user,
|
||||||
related_status=self.status
|
related_status=self.status,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
''' delete and delete notifications '''
|
""" delete and delete notifications """
|
||||||
# check for notification
|
# check for notification
|
||||||
if self.status.user.local:
|
if self.status.user.local:
|
||||||
notification_model = apps.get_model(
|
notification_model = apps.get_model(
|
||||||
'bookwyrm.Notification', require_ready=True)
|
"bookwyrm.Notification", require_ready=True
|
||||||
|
)
|
||||||
notification = notification_model.objects.filter(
|
notification = notification_model.objects.filter(
|
||||||
user=self.status.user, related_user=self.user,
|
user=self.status.user,
|
||||||
related_status=self.status, notification_type='FAVORITE'
|
related_user=self.user,
|
||||||
|
related_status=self.status,
|
||||||
|
notification_type="FAVORITE",
|
||||||
).first()
|
).first()
|
||||||
if notification:
|
if notification:
|
||||||
notification.delete()
|
notification.delete()
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' can't fav things twice '''
|
""" can't fav things twice """
|
||||||
unique_together = ('user', 'status')
|
|
||||||
|
unique_together = ("user", "status")
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
''' connections to external ActivityPub servers '''
|
""" connections to external ActivityPub servers """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
class FederatedServer(BookWyrmModel):
|
class FederatedServer(BookWyrmModel):
|
||||||
''' store which server's we federate with '''
|
""" store which server's we federate with """
|
||||||
|
|
||||||
server_name = models.CharField(max_length=255, unique=True)
|
server_name = models.CharField(max_length=255, unique=True)
|
||||||
# federated, blocked, whatever else
|
# federated, blocked, whatever else
|
||||||
status = models.CharField(max_length=255, default='federated')
|
status = models.CharField(max_length=255, default="federated")
|
||||||
# is it mastodon, bookwyrm, etc
|
# is it mastodon, bookwyrm, etc
|
||||||
application_type = models.CharField(max_length=255, null=True)
|
application_type = models.CharField(max_length=255, null=True)
|
||||||
application_version = models.CharField(max_length=255, null=True)
|
application_version = models.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
|
|
||||||
# TODO: blocked servers
|
# TODO: blocked servers
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' activitypub-aware django model fields '''
|
""" activitypub-aware django model fields """
|
||||||
from dataclasses import MISSING
|
from dataclasses import MISSING
|
||||||
import re
|
import re
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
@ -18,37 +18,43 @@ from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
def validate_remote_id(value):
|
def validate_remote_id(value):
|
||||||
''' make sure the remote_id looks like a url '''
|
""" make sure the remote_id looks like a url """
|
||||||
if not value or not re.match(r'^http.?:\/\/[^\s]+$', value):
|
if not value or not re.match(r"^http.?:\/\/[^\s]+$", value):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('%(value)s is not a valid remote_id'),
|
_("%(value)s is not a valid remote_id"),
|
||||||
params={'value': value},
|
params={"value": value},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_localname(value):
|
def validate_localname(value):
|
||||||
''' make sure localnames look okay '''
|
""" make sure localnames look okay """
|
||||||
if not re.match(r'^[A-Za-z\-_\.0-9]+$', value):
|
if not re.match(r"^[A-Za-z\-_\.0-9]+$", value):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('%(value)s is not a valid username'),
|
_("%(value)s is not a valid username"),
|
||||||
params={'value': value},
|
params={"value": value},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_username(value):
|
def validate_username(value):
|
||||||
''' make sure usernames look okay '''
|
""" make sure usernames look okay """
|
||||||
if not re.match(r'^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$', value):
|
if not re.match(r"^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$", value):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('%(value)s is not a valid username'),
|
_("%(value)s is not a valid username"),
|
||||||
params={'value': value},
|
params={"value": value},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ActivitypubFieldMixin:
|
class ActivitypubFieldMixin:
|
||||||
''' make a database field serializable '''
|
""" make a database field serializable """
|
||||||
def __init__(self, *args, \
|
|
||||||
activitypub_field=None, activitypub_wrapper=None,
|
def __init__(
|
||||||
deduplication_field=False, **kwargs):
|
self,
|
||||||
|
*args,
|
||||||
|
activitypub_field=None,
|
||||||
|
activitypub_wrapper=None,
|
||||||
|
deduplication_field=False,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
self.deduplication_field = deduplication_field
|
self.deduplication_field = deduplication_field
|
||||||
if activitypub_wrapper:
|
if activitypub_wrapper:
|
||||||
self.activitypub_wrapper = activitypub_field
|
self.activitypub_wrapper = activitypub_field
|
||||||
|
@ -57,24 +63,22 @@ class ActivitypubFieldMixin:
|
||||||
self.activitypub_field = activitypub_field
|
self.activitypub_field = activitypub_field
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def set_field_from_activity(self, instance, data):
|
def set_field_from_activity(self, instance, data):
|
||||||
''' helper function for assinging a value to the field '''
|
""" helper function for assinging a value to the field """
|
||||||
try:
|
try:
|
||||||
value = getattr(data, self.get_activitypub_field())
|
value = getattr(data, self.get_activitypub_field())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# masssively hack-y workaround for boosts
|
# masssively hack-y workaround for boosts
|
||||||
if self.get_activitypub_field() != 'attributedTo':
|
if self.get_activitypub_field() != "attributedTo":
|
||||||
raise
|
raise
|
||||||
value = getattr(data, 'actor')
|
value = getattr(data, "actor")
|
||||||
formatted = self.field_from_activity(value)
|
formatted = self.field_from_activity(value)
|
||||||
if formatted is None or formatted is MISSING:
|
if formatted is None or formatted is MISSING:
|
||||||
return
|
return
|
||||||
setattr(instance, self.name, formatted)
|
setattr(instance, self.name, formatted)
|
||||||
|
|
||||||
|
|
||||||
def set_activity_from_field(self, activity, instance):
|
def set_activity_from_field(self, activity, instance):
|
||||||
''' update the json object '''
|
""" update the json object """
|
||||||
value = getattr(instance, self.name)
|
value = getattr(instance, self.name)
|
||||||
formatted = self.field_to_activity(value)
|
formatted = self.field_to_activity(value)
|
||||||
if formatted is None:
|
if formatted is None:
|
||||||
|
@ -82,37 +86,37 @@ class ActivitypubFieldMixin:
|
||||||
|
|
||||||
key = self.get_activitypub_field()
|
key = self.get_activitypub_field()
|
||||||
# TODO: surely there's a better way
|
# TODO: surely there's a better way
|
||||||
if instance.__class__.__name__ == 'Boost' and key == 'attributedTo':
|
if instance.__class__.__name__ == "Boost" and key == "attributedTo":
|
||||||
key = 'actor'
|
key = "actor"
|
||||||
if isinstance(activity.get(key), list):
|
if isinstance(activity.get(key), list):
|
||||||
activity[key] += formatted
|
activity[key] += formatted
|
||||||
else:
|
else:
|
||||||
activity[key] = formatted
|
activity[key] = formatted
|
||||||
|
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
''' formatter to convert a model value into activitypub '''
|
""" formatter to convert a model value into activitypub """
|
||||||
if hasattr(self, 'activitypub_wrapper'):
|
if hasattr(self, "activitypub_wrapper"):
|
||||||
return {self.activitypub_wrapper: value}
|
return {self.activitypub_wrapper: value}
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def field_from_activity(self, value):
|
def field_from_activity(self, value):
|
||||||
''' formatter to convert activitypub into a model value '''
|
""" formatter to convert activitypub into a model value """
|
||||||
if hasattr(self, 'activitypub_wrapper'):
|
if hasattr(self, "activitypub_wrapper"):
|
||||||
value = value.get(self.activitypub_wrapper)
|
value = value.get(self.activitypub_wrapper)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def get_activitypub_field(self):
|
def get_activitypub_field(self):
|
||||||
''' model_field_name to activitypubFieldName '''
|
""" model_field_name to activitypubFieldName """
|
||||||
if self.activitypub_field:
|
if self.activitypub_field:
|
||||||
return self.activitypub_field
|
return self.activitypub_field
|
||||||
name = self.name.split('.')[-1]
|
name = self.name.split(".")[-1]
|
||||||
components = name.split('_')
|
components = name.split("_")
|
||||||
return components[0] + ''.join(x.title() for x in components[1:])
|
return components[0] + "".join(x.title() for x in components[1:])
|
||||||
|
|
||||||
|
|
||||||
class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
||||||
''' default (de)serialization for foreign key and one to one '''
|
""" default (de)serialization for foreign key and one to one """
|
||||||
|
|
||||||
def __init__(self, *args, load_remote=True, **kwargs):
|
def __init__(self, *args, load_remote=True, **kwargs):
|
||||||
self.load_remote = load_remote
|
self.load_remote = load_remote
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -122,7 +126,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
related_model = self.related_model
|
related_model = self.related_model
|
||||||
if hasattr(value, 'id') and value.id:
|
if hasattr(value, "id") and value.id:
|
||||||
if not self.load_remote:
|
if not self.load_remote:
|
||||||
# only look in the local database
|
# only look in the local database
|
||||||
return related_model.find_existing(value.serialize())
|
return related_model.find_existing(value.serialize())
|
||||||
|
@ -142,99 +146,98 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
||||||
|
|
||||||
|
|
||||||
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
|
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
|
||||||
''' a url that serves as a unique identifier '''
|
""" a url that serves as a unique identifier """
|
||||||
|
|
||||||
def __init__(self, *args, max_length=255, validators=None, **kwargs):
|
def __init__(self, *args, max_length=255, validators=None, **kwargs):
|
||||||
validators = validators or [validate_remote_id]
|
validators = validators or [validate_remote_id]
|
||||||
super().__init__(
|
super().__init__(*args, max_length=max_length, validators=validators, **kwargs)
|
||||||
*args, max_length=max_length, validators=validators,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
# for this field, the default is true. false everywhere else.
|
# for this field, the default is true. false everywhere else.
|
||||||
self.deduplication_field = kwargs.get('deduplication_field', True)
|
self.deduplication_field = kwargs.get("deduplication_field", True)
|
||||||
|
|
||||||
|
|
||||||
class UsernameField(ActivitypubFieldMixin, models.CharField):
|
class UsernameField(ActivitypubFieldMixin, models.CharField):
|
||||||
''' activitypub-aware username field '''
|
""" activitypub-aware username field """
|
||||||
def __init__(self, activitypub_field='preferredUsername', **kwargs):
|
|
||||||
|
def __init__(self, activitypub_field="preferredUsername", **kwargs):
|
||||||
self.activitypub_field = activitypub_field
|
self.activitypub_field = activitypub_field
|
||||||
# I don't totally know why pylint is mad at this, but it makes it work
|
# I don't totally know why pylint is mad at this, but it makes it work
|
||||||
super( #pylint: disable=bad-super-call
|
super(ActivitypubFieldMixin, self).__init__( # pylint: disable=bad-super-call
|
||||||
ActivitypubFieldMixin, self
|
_("username"),
|
||||||
).__init__(
|
|
||||||
_('username'),
|
|
||||||
max_length=150,
|
max_length=150,
|
||||||
unique=True,
|
unique=True,
|
||||||
validators=[validate_username],
|
validators=[validate_username],
|
||||||
error_messages={
|
error_messages={
|
||||||
'unique': _('A user with that username already exists.'),
|
"unique": _("A user with that username already exists."),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def deconstruct(self):
|
def deconstruct(self):
|
||||||
''' implementation of models.Field deconstruct '''
|
""" implementation of models.Field deconstruct """
|
||||||
name, path, args, kwargs = super().deconstruct()
|
name, path, args, kwargs = super().deconstruct()
|
||||||
del kwargs['verbose_name']
|
del kwargs["verbose_name"]
|
||||||
del kwargs['max_length']
|
del kwargs["max_length"]
|
||||||
del kwargs['unique']
|
del kwargs["unique"]
|
||||||
del kwargs['validators']
|
del kwargs["validators"]
|
||||||
del kwargs['error_messages']
|
del kwargs["error_messages"]
|
||||||
return name, path, args, kwargs
|
return name, path, args, kwargs
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
return value.split('@')[0]
|
return value.split("@")[0]
|
||||||
|
|
||||||
|
|
||||||
PrivacyLevels = models.TextChoices('Privacy', [
|
PrivacyLevels = models.TextChoices(
|
||||||
'public',
|
"Privacy", ["public", "unlisted", "followers", "direct"]
|
||||||
'unlisted',
|
)
|
||||||
'followers',
|
|
||||||
'direct'
|
|
||||||
])
|
|
||||||
|
|
||||||
class PrivacyField(ActivitypubFieldMixin, models.CharField):
|
class PrivacyField(ActivitypubFieldMixin, models.CharField):
|
||||||
''' this maps to two differente activitypub fields '''
|
""" this maps to two differente activitypub fields """
|
||||||
public = 'https://www.w3.org/ns/activitystreams#Public'
|
|
||||||
|
public = "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
*args, max_length=255,
|
*args, max_length=255, choices=PrivacyLevels.choices, default="public"
|
||||||
choices=PrivacyLevels.choices, default='public')
|
)
|
||||||
|
|
||||||
def set_field_from_activity(self, instance, data):
|
def set_field_from_activity(self, instance, data):
|
||||||
to = data.to
|
to = data.to
|
||||||
cc = data.cc
|
cc = data.cc
|
||||||
if to == [self.public]:
|
if to == [self.public]:
|
||||||
setattr(instance, self.name, 'public')
|
setattr(instance, self.name, "public")
|
||||||
elif cc == []:
|
elif cc == []:
|
||||||
setattr(instance, self.name, 'direct')
|
setattr(instance, self.name, "direct")
|
||||||
elif self.public in cc:
|
elif self.public in cc:
|
||||||
setattr(instance, self.name, 'unlisted')
|
setattr(instance, self.name, "unlisted")
|
||||||
else:
|
else:
|
||||||
setattr(instance, self.name, 'followers')
|
setattr(instance, self.name, "followers")
|
||||||
|
|
||||||
def set_activity_from_field(self, activity, instance):
|
def set_activity_from_field(self, activity, instance):
|
||||||
# explicitly to anyone mentioned (statuses only)
|
# explicitly to anyone mentioned (statuses only)
|
||||||
mentions = []
|
mentions = []
|
||||||
if hasattr(instance, 'mention_users'):
|
if hasattr(instance, "mention_users"):
|
||||||
mentions = [u.remote_id for u in instance.mention_users.all()]
|
mentions = [u.remote_id for u in instance.mention_users.all()]
|
||||||
# this is a link to the followers list
|
# this is a link to the followers list
|
||||||
followers = instance.user.__class__._meta.get_field('followers')\
|
followers = instance.user.__class__._meta.get_field(
|
||||||
.field_to_activity(instance.user.followers)
|
"followers"
|
||||||
if instance.privacy == 'public':
|
).field_to_activity(instance.user.followers)
|
||||||
activity['to'] = [self.public]
|
if instance.privacy == "public":
|
||||||
activity['cc'] = [followers] + mentions
|
activity["to"] = [self.public]
|
||||||
elif instance.privacy == 'unlisted':
|
activity["cc"] = [followers] + mentions
|
||||||
activity['to'] = [followers]
|
elif instance.privacy == "unlisted":
|
||||||
activity['cc'] = [self.public] + mentions
|
activity["to"] = [followers]
|
||||||
elif instance.privacy == 'followers':
|
activity["cc"] = [self.public] + mentions
|
||||||
activity['to'] = [followers]
|
elif instance.privacy == "followers":
|
||||||
activity['cc'] = mentions
|
activity["to"] = [followers]
|
||||||
if instance.privacy == 'direct':
|
activity["cc"] = mentions
|
||||||
activity['to'] = mentions
|
if instance.privacy == "direct":
|
||||||
activity['cc'] = []
|
activity["to"] = mentions
|
||||||
|
activity["cc"] = []
|
||||||
|
|
||||||
|
|
||||||
class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
|
class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
|
||||||
''' activitypub-aware foreign key field '''
|
""" activitypub-aware foreign key field """
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
|
@ -242,7 +245,8 @@ class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
|
||||||
|
|
||||||
|
|
||||||
class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
|
class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
|
||||||
''' activitypub-aware foreign key field '''
|
""" activitypub-aware foreign key field """
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
|
@ -250,13 +254,14 @@ class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
|
||||||
|
|
||||||
|
|
||||||
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||||
''' activitypub-aware many to many field '''
|
""" activitypub-aware many to many field """
|
||||||
|
|
||||||
def __init__(self, *args, link_only=False, **kwargs):
|
def __init__(self, *args, link_only=False, **kwargs):
|
||||||
self.link_only = link_only
|
self.link_only = link_only
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def set_field_from_activity(self, instance, data):
|
def set_field_from_activity(self, instance, data):
|
||||||
''' helper function for assinging a value to the field '''
|
""" helper function for assinging a value to the field """
|
||||||
value = getattr(data, self.get_activitypub_field())
|
value = getattr(data, self.get_activitypub_field())
|
||||||
formatted = self.field_from_activity(value)
|
formatted = self.field_from_activity(value)
|
||||||
if formatted is None or formatted is MISSING:
|
if formatted is None or formatted is MISSING:
|
||||||
|
@ -266,7 +271,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
if self.link_only:
|
if self.link_only:
|
||||||
return '%s/%s' % (value.instance.remote_id, self.name)
|
return "%s/%s" % (value.instance.remote_id, self.name)
|
||||||
return [i.remote_id for i in value.all()]
|
return [i.remote_id for i in value.all()]
|
||||||
|
|
||||||
def field_from_activity(self, value):
|
def field_from_activity(self, value):
|
||||||
|
@ -279,29 +284,31 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
continue
|
continue
|
||||||
items.append(
|
items.append(
|
||||||
activitypub.resolve_remote_id(
|
activitypub.resolve_remote_id(remote_id, model=self.related_model)
|
||||||
remote_id, model=self.related_model)
|
|
||||||
)
|
)
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
class TagField(ManyToManyField):
|
class TagField(ManyToManyField):
|
||||||
''' special case of many to many that uses Tags '''
|
""" special case of many to many that uses Tags """
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.activitypub_field = 'tag'
|
self.activitypub_field = "tag"
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
tags = []
|
tags = []
|
||||||
for item in value.all():
|
for item in value.all():
|
||||||
activity_type = item.__class__.__name__
|
activity_type = item.__class__.__name__
|
||||||
if activity_type == 'User':
|
if activity_type == "User":
|
||||||
activity_type = 'Mention'
|
activity_type = "Mention"
|
||||||
tags.append(activitypub.Link(
|
tags.append(
|
||||||
|
activitypub.Link(
|
||||||
href=item.remote_id,
|
href=item.remote_id,
|
||||||
name=getattr(item, item.name_field),
|
name=getattr(item, item.name_field),
|
||||||
type=activity_type
|
type=activity_type,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
return tags
|
return tags
|
||||||
|
|
||||||
def field_from_activity(self, value):
|
def field_from_activity(self, value):
|
||||||
|
@ -310,38 +317,38 @@ class TagField(ManyToManyField):
|
||||||
items = []
|
items = []
|
||||||
for link_json in value:
|
for link_json in value:
|
||||||
link = activitypub.Link(**link_json)
|
link = activitypub.Link(**link_json)
|
||||||
tag_type = link.type if link.type != 'Mention' else 'Person'
|
tag_type = link.type if link.type != "Mention" else "Person"
|
||||||
if tag_type == 'Book':
|
if tag_type == "Book":
|
||||||
tag_type = 'Edition'
|
tag_type = "Edition"
|
||||||
if tag_type != self.related_model.activity_serializer.type:
|
if tag_type != self.related_model.activity_serializer.type:
|
||||||
# tags can contain multiple types
|
# tags can contain multiple types
|
||||||
continue
|
continue
|
||||||
items.append(
|
items.append(
|
||||||
activitypub.resolve_remote_id(
|
activitypub.resolve_remote_id(link.href, model=self.related_model)
|
||||||
link.href, model=self.related_model)
|
|
||||||
)
|
)
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def image_serializer(value, alt):
|
def image_serializer(value, alt):
|
||||||
''' helper for serializing images '''
|
""" helper for serializing images """
|
||||||
if value and hasattr(value, 'url'):
|
if value and hasattr(value, "url"):
|
||||||
url = value.url
|
url = value.url
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
url = 'https://%s%s' % (DOMAIN, url)
|
url = "https://%s%s" % (DOMAIN, url)
|
||||||
return activitypub.Image(url=url, name=alt)
|
return activitypub.Image(url=url, name=alt)
|
||||||
|
|
||||||
|
|
||||||
class ImageField(ActivitypubFieldMixin, models.ImageField):
|
class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||||
''' activitypub-aware image field '''
|
""" activitypub-aware image field """
|
||||||
|
|
||||||
def __init__(self, *args, alt_field=None, **kwargs):
|
def __init__(self, *args, alt_field=None, **kwargs):
|
||||||
self.alt_field = alt_field
|
self.alt_field = alt_field
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# pylint: disable=arguments-differ
|
# pylint: disable=arguments-differ
|
||||||
def set_field_from_activity(self, instance, data, save=True):
|
def set_field_from_activity(self, instance, data, save=True):
|
||||||
''' helper function for assinging a value to the field '''
|
""" helper function for assinging a value to the field """
|
||||||
value = getattr(data, self.get_activitypub_field())
|
value = getattr(data, self.get_activitypub_field())
|
||||||
formatted = self.field_from_activity(value)
|
formatted = self.field_from_activity(value)
|
||||||
if formatted is None or formatted is MISSING:
|
if formatted is None or formatted is MISSING:
|
||||||
|
@ -358,16 +365,14 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||||
key = self.get_activitypub_field()
|
key = self.get_activitypub_field()
|
||||||
activity[key] = formatted
|
activity[key] = formatted
|
||||||
|
|
||||||
|
|
||||||
def field_to_activity(self, value, alt=None):
|
def field_to_activity(self, value, alt=None):
|
||||||
return image_serializer(value, alt)
|
return image_serializer(value, alt)
|
||||||
|
|
||||||
|
|
||||||
def field_from_activity(self, value):
|
def field_from_activity(self, value):
|
||||||
image_slug = value
|
image_slug = value
|
||||||
# when it's an inline image (User avatar/icon, Book cover), it's a json
|
# 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
|
# blob, but when it's an attached image, it's just a url
|
||||||
if hasattr(image_slug, 'url'):
|
if hasattr(image_slug, "url"):
|
||||||
url = image_slug.url
|
url = image_slug.url
|
||||||
elif isinstance(image_slug, str):
|
elif isinstance(image_slug, str):
|
||||||
url = image_slug
|
url = image_slug
|
||||||
|
@ -383,13 +388,14 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||||
if not response:
|
if not response:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
image_name = str(uuid4()) + '.' + url.split('.')[-1]
|
image_name = str(uuid4()) + "." + url.split(".")[-1]
|
||||||
image_content = ContentFile(response.content)
|
image_content = ContentFile(response.content)
|
||||||
return [image_name, image_content]
|
return [image_name, image_content]
|
||||||
|
|
||||||
|
|
||||||
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
|
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
|
||||||
''' activitypub-aware datetime field '''
|
""" activitypub-aware datetime field """
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
|
@ -405,8 +411,10 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
|
||||||
except (ParserError, TypeError):
|
except (ParserError, TypeError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class HtmlField(ActivitypubFieldMixin, models.TextField):
|
class HtmlField(ActivitypubFieldMixin, models.TextField):
|
||||||
''' a text field for storing html '''
|
""" a text field for storing html """
|
||||||
|
|
||||||
def field_from_activity(self, value):
|
def field_from_activity(self, value):
|
||||||
if not value or value == MISSING:
|
if not value or value == MISSING:
|
||||||
return None
|
return None
|
||||||
|
@ -414,19 +422,25 @@ class HtmlField(ActivitypubFieldMixin, models.TextField):
|
||||||
sanitizer.feed(value)
|
sanitizer.feed(value)
|
||||||
return sanitizer.get_output()
|
return sanitizer.get_output()
|
||||||
|
|
||||||
|
|
||||||
class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
|
class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
|
||||||
''' activitypub-aware array field '''
|
""" activitypub-aware array field """
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
return [str(i) for i in value]
|
return [str(i) for i in value]
|
||||||
|
|
||||||
|
|
||||||
class CharField(ActivitypubFieldMixin, models.CharField):
|
class CharField(ActivitypubFieldMixin, models.CharField):
|
||||||
''' activitypub-aware char field '''
|
""" activitypub-aware char field """
|
||||||
|
|
||||||
|
|
||||||
class TextField(ActivitypubFieldMixin, models.TextField):
|
class TextField(ActivitypubFieldMixin, models.TextField):
|
||||||
''' activitypub-aware text field '''
|
""" activitypub-aware text field """
|
||||||
|
|
||||||
|
|
||||||
class BooleanField(ActivitypubFieldMixin, models.BooleanField):
|
class BooleanField(ActivitypubFieldMixin, models.BooleanField):
|
||||||
''' activitypub-aware boolean field '''
|
""" activitypub-aware boolean field """
|
||||||
|
|
||||||
|
|
||||||
class IntegerField(ActivitypubFieldMixin, models.IntegerField):
|
class IntegerField(ActivitypubFieldMixin, models.IntegerField):
|
||||||
''' activitypub-aware boolean field '''
|
""" activitypub-aware boolean field """
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' track progress of goodreads imports '''
|
""" track progress of goodreads imports """
|
||||||
import re
|
import re
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
|
|
||||||
|
@ -14,13 +14,14 @@ from .fields import PrivacyLevels
|
||||||
|
|
||||||
# Mapping goodreads -> bookwyrm shelf titles.
|
# Mapping goodreads -> bookwyrm shelf titles.
|
||||||
GOODREADS_SHELVES = {
|
GOODREADS_SHELVES = {
|
||||||
'read': 'read',
|
"read": "read",
|
||||||
'currently-reading': 'reading',
|
"currently-reading": "reading",
|
||||||
'to-read': 'to-read',
|
"to-read": "to-read",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def unquote_string(text):
|
def unquote_string(text):
|
||||||
''' resolve csv quote weirdness '''
|
""" resolve csv quote weirdness """
|
||||||
match = re.match(r'="([^"]*)"', text)
|
match = re.match(r'="([^"]*)"', text)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
|
@ -28,63 +29,57 @@ def unquote_string(text):
|
||||||
|
|
||||||
|
|
||||||
def construct_search_term(title, author):
|
def construct_search_term(title, author):
|
||||||
''' formulate a query for the data connector '''
|
""" formulate a query for the data connector """
|
||||||
# Strip brackets (usually series title from search term)
|
# Strip brackets (usually series title from search term)
|
||||||
title = re.sub(r'\s*\([^)]*\)\s*', '', title)
|
title = re.sub(r"\s*\([^)]*\)\s*", "", title)
|
||||||
# Open library doesn't like including author initials in search term.
|
# Open library doesn't like including author initials in search term.
|
||||||
author = re.sub(r'(\w\.)+\s*', '', author)
|
author = re.sub(r"(\w\.)+\s*", "", author)
|
||||||
|
|
||||||
return ' '.join([title, author])
|
return " ".join([title, author])
|
||||||
|
|
||||||
|
|
||||||
class ImportJob(models.Model):
|
class ImportJob(models.Model):
|
||||||
''' entry for a specific request for book data import '''
|
""" entry for a specific request for book data import """
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
created_date = models.DateTimeField(default=timezone.now)
|
created_date = models.DateTimeField(default=timezone.now)
|
||||||
task_id = models.CharField(max_length=100, null=True)
|
task_id = models.CharField(max_length=100, null=True)
|
||||||
include_reviews = models.BooleanField(default=True)
|
include_reviews = models.BooleanField(default=True)
|
||||||
complete = models.BooleanField(default=False)
|
complete = models.BooleanField(default=False)
|
||||||
privacy = models.CharField(
|
privacy = models.CharField(
|
||||||
max_length=255,
|
max_length=255, default="public", choices=PrivacyLevels.choices
|
||||||
default='public',
|
|
||||||
choices=PrivacyLevels.choices
|
|
||||||
)
|
)
|
||||||
retry = models.BooleanField(default=False)
|
retry = models.BooleanField(default=False)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' save and notify '''
|
""" save and notify """
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if self.complete:
|
if self.complete:
|
||||||
notification_model = apps.get_model(
|
notification_model = apps.get_model(
|
||||||
'bookwyrm.Notification', require_ready=True)
|
"bookwyrm.Notification", require_ready=True
|
||||||
|
)
|
||||||
notification_model.objects.create(
|
notification_model.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
notification_type='IMPORT',
|
notification_type="IMPORT",
|
||||||
related_import=self,
|
related_import=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ImportItem(models.Model):
|
class ImportItem(models.Model):
|
||||||
''' a single line of a csv being imported '''
|
""" a single line of a csv being imported """
|
||||||
job = models.ForeignKey(
|
|
||||||
ImportJob,
|
job = models.ForeignKey(ImportJob, on_delete=models.CASCADE, related_name="items")
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='items')
|
|
||||||
index = models.IntegerField()
|
index = models.IntegerField()
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
book = models.ForeignKey(
|
book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
Book, on_delete=models.SET_NULL, null=True, blank=True)
|
|
||||||
fail_reason = models.TextField(null=True)
|
fail_reason = models.TextField(null=True)
|
||||||
|
|
||||||
def resolve(self):
|
def resolve(self):
|
||||||
''' try various ways to lookup a book '''
|
""" try various ways to lookup a book """
|
||||||
self.book = (
|
self.book = self.get_book_from_isbn() or self.get_book_from_title_author()
|
||||||
self.get_book_from_isbn() or
|
|
||||||
self.get_book_from_title_author()
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_book_from_isbn(self):
|
def get_book_from_isbn(self):
|
||||||
''' search by isbn '''
|
""" search by isbn """
|
||||||
search_result = connector_manager.first_search_result(
|
search_result = connector_manager.first_search_result(
|
||||||
self.isbn, min_confidence=0.999
|
self.isbn, min_confidence=0.999
|
||||||
)
|
)
|
||||||
|
@ -93,13 +88,9 @@ class ImportItem(models.Model):
|
||||||
return search_result.connector.get_or_create_book(search_result.key)
|
return search_result.connector.get_or_create_book(search_result.key)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_book_from_title_author(self):
|
def get_book_from_title_author(self):
|
||||||
''' search by title and author '''
|
""" search by title and author """
|
||||||
search_term = construct_search_term(
|
search_term = construct_search_term(self.title, self.author)
|
||||||
self.title,
|
|
||||||
self.author
|
|
||||||
)
|
|
||||||
search_result = connector_manager.first_search_result(
|
search_result = connector_manager.first_search_result(
|
||||||
search_term, min_confidence=0.999
|
search_term, min_confidence=0.999
|
||||||
)
|
)
|
||||||
|
@ -108,84 +99,85 @@ class ImportItem(models.Model):
|
||||||
return search_result.connector.get_or_create_book(search_result.key)
|
return search_result.connector.get_or_create_book(search_result.key)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
''' get the book title '''
|
""" get the book title """
|
||||||
return self.data['Title']
|
return self.data["Title"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def author(self):
|
def author(self):
|
||||||
''' get the book title '''
|
""" get the book title """
|
||||||
return self.data['Author']
|
return self.data["Author"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isbn(self):
|
def isbn(self):
|
||||||
''' pulls out the isbn13 field from the csv line data '''
|
""" pulls out the isbn13 field from the csv line data """
|
||||||
return unquote_string(self.data['ISBN13'])
|
return unquote_string(self.data["ISBN13"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shelf(self):
|
def shelf(self):
|
||||||
''' the goodreads shelf field '''
|
""" the goodreads shelf field """
|
||||||
if self.data['Exclusive Shelf']:
|
if self.data["Exclusive Shelf"]:
|
||||||
return GOODREADS_SHELVES.get(self.data['Exclusive Shelf'])
|
return GOODREADS_SHELVES.get(self.data["Exclusive Shelf"])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def review(self):
|
def review(self):
|
||||||
''' a user-written review, to be imported with the book data '''
|
""" a user-written review, to be imported with the book data """
|
||||||
return self.data['My Review']
|
return self.data["My Review"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rating(self):
|
def rating(self):
|
||||||
''' x/5 star rating for a book '''
|
""" x/5 star rating for a book """
|
||||||
return int(self.data['My Rating'])
|
return int(self.data["My Rating"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_added(self):
|
def date_added(self):
|
||||||
''' when the book was added to this dataset '''
|
""" when the book was added to this dataset """
|
||||||
if self.data['Date Added']:
|
if self.data["Date Added"]:
|
||||||
return timezone.make_aware(
|
return timezone.make_aware(dateutil.parser.parse(self.data["Date Added"]))
|
||||||
dateutil.parser.parse(self.data['Date Added']))
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_started(self):
|
def date_started(self):
|
||||||
''' when the book was started '''
|
""" when the book was started """
|
||||||
if "Date Started" in self.data and self.data['Date Started']:
|
if "Date Started" in self.data and self.data["Date Started"]:
|
||||||
return timezone.make_aware(
|
return timezone.make_aware(dateutil.parser.parse(self.data["Date Started"]))
|
||||||
dateutil.parser.parse(self.data['Date Started']))
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_read(self):
|
def date_read(self):
|
||||||
''' the date a book was completed '''
|
""" the date a book was completed """
|
||||||
if self.data['Date Read']:
|
if self.data["Date Read"]:
|
||||||
return timezone.make_aware(
|
return timezone.make_aware(dateutil.parser.parse(self.data["Date Read"]))
|
||||||
dateutil.parser.parse(self.data['Date Read']))
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reads(self):
|
def reads(self):
|
||||||
''' formats a read through dataset for the book in this line '''
|
""" formats a read through dataset for the book in this line """
|
||||||
start_date = self.date_started
|
start_date = self.date_started
|
||||||
|
|
||||||
# Goodreads special case (no 'date started' field)
|
# Goodreads special case (no 'date started' field)
|
||||||
if ((self.shelf == 'reading' or (self.shelf == 'read' and self.date_read))
|
if (
|
||||||
and self.date_added and not start_date):
|
(self.shelf == "reading" or (self.shelf == "read" and self.date_read))
|
||||||
|
and self.date_added
|
||||||
|
and not start_date
|
||||||
|
):
|
||||||
start_date = self.date_added
|
start_date = self.date_added
|
||||||
|
|
||||||
if (start_date and start_date is not None and not self.date_read):
|
if start_date and start_date is not None and not self.date_read:
|
||||||
return [ReadThrough(start_date=start_date)]
|
return [ReadThrough(start_date=start_date)]
|
||||||
if self.date_read:
|
if self.date_read:
|
||||||
return [ReadThrough(
|
return [
|
||||||
|
ReadThrough(
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
finish_date=self.date_read,
|
finish_date=self.date_read,
|
||||||
)]
|
)
|
||||||
|
]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{!r}Item {!r}>".format(self.data['import_source'], self.data['Title'])
|
return "<{!r}Item {!r}>".format(self.data["import_source"], self.data["Title"])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} by {}".format(self.data['Title'], self.data['Author'])
|
return "{} by {}".format(self.data["Title"], self.data["Author"])
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' make a list of books!! '''
|
""" make a list of books!! """
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
@ -9,86 +9,89 @@ from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|
||||||
CurationType = models.TextChoices('Curation', [
|
CurationType = models.TextChoices(
|
||||||
'closed',
|
"Curation",
|
||||||
'open',
|
[
|
||||||
'curated',
|
"closed",
|
||||||
])
|
"open",
|
||||||
|
"curated",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class List(OrderedCollectionMixin, BookWyrmModel):
|
class List(OrderedCollectionMixin, BookWyrmModel):
|
||||||
''' a list of books '''
|
""" a list of books """
|
||||||
|
|
||||||
name = fields.CharField(max_length=100)
|
name = fields.CharField(max_length=100)
|
||||||
user = fields.ForeignKey(
|
user = fields.ForeignKey(
|
||||||
'User', on_delete=models.PROTECT, activitypub_field='owner')
|
"User", on_delete=models.PROTECT, activitypub_field="owner"
|
||||||
description = fields.TextField(
|
)
|
||||||
blank=True, null=True, activitypub_field='summary')
|
description = fields.TextField(blank=True, null=True, activitypub_field="summary")
|
||||||
privacy = fields.PrivacyField()
|
privacy = fields.PrivacyField()
|
||||||
curation = fields.CharField(
|
curation = fields.CharField(
|
||||||
max_length=255,
|
max_length=255, default="closed", choices=CurationType.choices
|
||||||
default='closed',
|
|
||||||
choices=CurationType.choices
|
|
||||||
)
|
)
|
||||||
books = models.ManyToManyField(
|
books = models.ManyToManyField(
|
||||||
'Edition',
|
"Edition",
|
||||||
symmetrical=False,
|
symmetrical=False,
|
||||||
through='ListItem',
|
through="ListItem",
|
||||||
through_fields=('book_list', 'book'),
|
through_fields=("book_list", "book"),
|
||||||
)
|
)
|
||||||
activity_serializer = activitypub.BookList
|
activity_serializer = activitypub.BookList
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
''' don't want the user to be in there in this case '''
|
""" don't want the user to be in there in this case """
|
||||||
return 'https://%s/list/%d' % (DOMAIN, self.id)
|
return "https://%s/list/%d" % (DOMAIN, self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collection_queryset(self):
|
def collection_queryset(self):
|
||||||
''' list of books for this shelf, overrides OrderedCollectionMixin '''
|
""" list of books for this shelf, overrides OrderedCollectionMixin """
|
||||||
return self.books.filter(
|
return self.books.filter(listitem__approved=True).all().order_by("listitem")
|
||||||
listitem__approved=True
|
|
||||||
).all().order_by('listitem')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' default sorting '''
|
""" default sorting """
|
||||||
ordering = ('-updated_date',)
|
|
||||||
|
ordering = ("-updated_date",)
|
||||||
|
|
||||||
|
|
||||||
class ListItem(CollectionItemMixin, BookWyrmModel):
|
class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||||
''' ok '''
|
""" ok """
|
||||||
|
|
||||||
book = fields.ForeignKey(
|
book = fields.ForeignKey(
|
||||||
'Edition', on_delete=models.PROTECT, activitypub_field='object')
|
"Edition", on_delete=models.PROTECT, activitypub_field="object"
|
||||||
|
)
|
||||||
book_list = fields.ForeignKey(
|
book_list = fields.ForeignKey(
|
||||||
'List', on_delete=models.CASCADE, activitypub_field='target')
|
"List", on_delete=models.CASCADE, activitypub_field="target"
|
||||||
|
)
|
||||||
user = fields.ForeignKey(
|
user = fields.ForeignKey(
|
||||||
'User',
|
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
||||||
on_delete=models.PROTECT,
|
|
||||||
activitypub_field='actor'
|
|
||||||
)
|
)
|
||||||
notes = fields.TextField(blank=True, null=True)
|
notes = fields.TextField(blank=True, null=True)
|
||||||
approved = models.BooleanField(default=True)
|
approved = models.BooleanField(default=True)
|
||||||
order = fields.IntegerField(blank=True, null=True)
|
order = fields.IntegerField(blank=True, null=True)
|
||||||
endorsement = models.ManyToManyField('User', related_name='endorsers')
|
endorsement = models.ManyToManyField("User", related_name="endorsers")
|
||||||
|
|
||||||
activity_serializer = activitypub.Add
|
activity_serializer = activitypub.Add
|
||||||
object_field = 'book'
|
object_field = "book"
|
||||||
collection_field = 'book_list'
|
collection_field = "book_list"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' create a notification too '''
|
""" create a notification too """
|
||||||
created = not bool(self.id)
|
created = not bool(self.id)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
list_owner = self.book_list.user
|
list_owner = self.book_list.user
|
||||||
# create a notification if somoene ELSE added to a local user's list
|
# create a notification if somoene ELSE added to a local user's list
|
||||||
if created and list_owner.local and list_owner != self.user:
|
if created and list_owner.local and list_owner != self.user:
|
||||||
model = apps.get_model('bookwyrm.Notification', require_ready=True)
|
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||||
model.objects.create(
|
model.objects.create(
|
||||||
user=list_owner,
|
user=list_owner,
|
||||||
related_user=self.user,
|
related_user=self.user,
|
||||||
related_list_item=self,
|
related_list_item=self,
|
||||||
notification_type='ADD',
|
notification_type="ADD",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' an opinionated constraint! you can't put a book on a list twice '''
|
""" an opinionated constraint! you can't put a book on a list twice """
|
||||||
unique_together = ('book', 'book_list')
|
|
||||||
ordering = ('-created_date',)
|
unique_together = ("book", "book_list")
|
||||||
|
ordering = ("-created_date",)
|
||||||
|
|
|
@ -1,32 +1,34 @@
|
||||||
''' alert a user to activity '''
|
""" alert a user to activity """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
NotificationType = models.TextChoices(
|
NotificationType = models.TextChoices(
|
||||||
'NotificationType',
|
"NotificationType",
|
||||||
'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD')
|
"FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Notification(BookWyrmModel):
|
class Notification(BookWyrmModel):
|
||||||
''' you've been tagged, liked, followed, etc '''
|
""" you've been tagged, liked, followed, etc """
|
||||||
user = models.ForeignKey('User', on_delete=models.CASCADE)
|
|
||||||
related_book = models.ForeignKey(
|
user = models.ForeignKey("User", on_delete=models.CASCADE)
|
||||||
'Edition', on_delete=models.CASCADE, null=True)
|
related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True)
|
||||||
related_user = models.ForeignKey(
|
related_user = models.ForeignKey(
|
||||||
'User',
|
"User", on_delete=models.CASCADE, null=True, related_name="related_user"
|
||||||
on_delete=models.CASCADE, null=True, related_name='related_user')
|
)
|
||||||
related_status = models.ForeignKey(
|
related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True)
|
||||||
'Status', on_delete=models.CASCADE, null=True)
|
related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True)
|
||||||
related_import = models.ForeignKey(
|
|
||||||
'ImportJob', on_delete=models.CASCADE, null=True)
|
|
||||||
related_list_item = models.ForeignKey(
|
related_list_item = models.ForeignKey(
|
||||||
'ListItem', on_delete=models.CASCADE, null=True)
|
"ListItem", on_delete=models.CASCADE, null=True
|
||||||
|
)
|
||||||
read = models.BooleanField(default=False)
|
read = models.BooleanField(default=False)
|
||||||
notification_type = models.CharField(
|
notification_type = models.CharField(
|
||||||
max_length=255, choices=NotificationType.choices)
|
max_length=255, choices=NotificationType.choices
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' save, but don't make dupes '''
|
""" save, but don't make dupes """
|
||||||
# there's probably a better way to do this
|
# there's probably a better way to do this
|
||||||
if self.__class__.objects.filter(
|
if self.__class__.objects.filter(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
@ -41,7 +43,8 @@ class Notification(BookWyrmModel):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' checks if notifcation is in enum list for valid types '''
|
""" checks if notifcation is in enum list for valid types """
|
||||||
|
|
||||||
constraints = [
|
constraints = [
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
check=models.Q(notification_type__in=NotificationType.values),
|
check=models.Q(notification_type__in=NotificationType.values),
|
||||||
|
|
|
@ -1,35 +1,32 @@
|
||||||
''' progress in a book '''
|
""" progress in a book """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
|
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
class ProgressMode(models.TextChoices):
|
class ProgressMode(models.TextChoices):
|
||||||
PAGE = 'PG', 'page'
|
PAGE = "PG", "page"
|
||||||
PERCENT = 'PCT', 'percent'
|
PERCENT = "PCT", "percent"
|
||||||
|
|
||||||
|
|
||||||
class ReadThrough(BookWyrmModel):
|
class ReadThrough(BookWyrmModel):
|
||||||
''' Store a read through a book in the database. '''
|
""" Store a read through a book in the database. """
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
|
||||||
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||||
|
book = models.ForeignKey("Edition", on_delete=models.PROTECT)
|
||||||
progress = models.IntegerField(
|
progress = models.IntegerField(
|
||||||
validators=[validators.MinValueValidator(0)],
|
validators=[validators.MinValueValidator(0)], null=True, blank=True
|
||||||
null=True,
|
)
|
||||||
blank=True)
|
|
||||||
progress_mode = models.CharField(
|
progress_mode = models.CharField(
|
||||||
max_length=3,
|
max_length=3, choices=ProgressMode.choices, default=ProgressMode.PAGE
|
||||||
choices=ProgressMode.choices,
|
)
|
||||||
default=ProgressMode.PAGE)
|
start_date = models.DateTimeField(blank=True, null=True)
|
||||||
start_date = models.DateTimeField(
|
finish_date = models.DateTimeField(blank=True, null=True)
|
||||||
blank=True,
|
|
||||||
null=True)
|
|
||||||
finish_date = models.DateTimeField(
|
|
||||||
blank=True,
|
|
||||||
null=True)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' update user active time '''
|
""" update user active time """
|
||||||
self.user.last_active_date = timezone.now()
|
self.user.last_active_date = timezone.now()
|
||||||
self.user.save(broadcast=False)
|
self.user.save(broadcast=False)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
@ -37,22 +34,22 @@ class ReadThrough(BookWyrmModel):
|
||||||
def create_update(self):
|
def create_update(self):
|
||||||
if self.progress:
|
if self.progress:
|
||||||
return self.progressupdate_set.create(
|
return self.progressupdate_set.create(
|
||||||
user=self.user,
|
user=self.user, progress=self.progress, mode=self.progress_mode
|
||||||
progress=self.progress,
|
)
|
||||||
mode=self.progress_mode)
|
|
||||||
|
|
||||||
class ProgressUpdate(BookWyrmModel):
|
class ProgressUpdate(BookWyrmModel):
|
||||||
''' Store progress through a book in the database. '''
|
""" Store progress through a book in the database. """
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
|
||||||
readthrough = models.ForeignKey('ReadThrough', on_delete=models.CASCADE)
|
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||||
|
readthrough = models.ForeignKey("ReadThrough", on_delete=models.CASCADE)
|
||||||
progress = models.IntegerField(validators=[validators.MinValueValidator(0)])
|
progress = models.IntegerField(validators=[validators.MinValueValidator(0)])
|
||||||
mode = models.CharField(
|
mode = models.CharField(
|
||||||
max_length=3,
|
max_length=3, choices=ProgressMode.choices, default=ProgressMode.PAGE
|
||||||
choices=ProgressMode.choices,
|
)
|
||||||
default=ProgressMode.PAGE)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' update user active time '''
|
""" update user active time """
|
||||||
self.user.last_active_date = timezone.now()
|
self.user.last_active_date = timezone.now()
|
||||||
self.user.save(broadcast=False)
|
self.user.save(broadcast=False)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' defines relationships between users '''
|
""" defines relationships between users """
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models, transaction, IntegrityError
|
from django.db import models, transaction, IntegrityError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
@ -11,67 +11,70 @@ from . import fields
|
||||||
|
|
||||||
|
|
||||||
class UserRelationship(BookWyrmModel):
|
class UserRelationship(BookWyrmModel):
|
||||||
''' many-to-many through table for followers '''
|
""" many-to-many through table for followers """
|
||||||
|
|
||||||
user_subject = fields.ForeignKey(
|
user_subject = fields.ForeignKey(
|
||||||
'User',
|
"User",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='%(class)s_user_subject',
|
related_name="%(class)s_user_subject",
|
||||||
activitypub_field='actor',
|
activitypub_field="actor",
|
||||||
)
|
)
|
||||||
user_object = fields.ForeignKey(
|
user_object = fields.ForeignKey(
|
||||||
'User',
|
"User",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='%(class)s_user_object',
|
related_name="%(class)s_user_object",
|
||||||
activitypub_field='object',
|
activitypub_field="object",
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def privacy(self):
|
def privacy(self):
|
||||||
''' all relationships are handled directly with the participants '''
|
""" all relationships are handled directly with the participants """
|
||||||
return 'direct'
|
return "direct"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def recipients(self):
|
def recipients(self):
|
||||||
''' the remote user needs to recieve direct broadcasts '''
|
""" the remote user needs to recieve direct broadcasts """
|
||||||
return [u for u in [self.user_subject, self.user_object] if not u.local]
|
return [u for u in [self.user_subject, self.user_object] if not u.local]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' relationships should be unique '''
|
""" relationships should be unique """
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=['user_subject', 'user_object'],
|
fields=["user_subject", "user_object"], name="%(class)s_unique"
|
||||||
name='%(class)s_unique'
|
|
||||||
),
|
),
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
check=~models.Q(user_subject=models.F('user_object')),
|
check=~models.Q(user_subject=models.F("user_object")),
|
||||||
name='%(class)s_no_self'
|
name="%(class)s_no_self",
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_remote_id(self, status=None): # pylint: disable=arguments-differ
|
def get_remote_id(self, status=None): # pylint: disable=arguments-differ
|
||||||
''' use shelf identifier in remote_id '''
|
""" use shelf identifier in remote_id """
|
||||||
status = status or 'follows'
|
status = status or "follows"
|
||||||
base_path = self.user_subject.remote_id
|
base_path = self.user_subject.remote_id
|
||||||
return '%s#%s/%d' % (base_path, status, self.id)
|
return "%s#%s/%d" % (base_path, status, self.id)
|
||||||
|
|
||||||
|
|
||||||
class UserFollows(ActivityMixin, UserRelationship):
|
class UserFollows(ActivityMixin, UserRelationship):
|
||||||
''' Following a user '''
|
""" Following a user """
|
||||||
status = 'follows'
|
|
||||||
|
status = "follows"
|
||||||
|
|
||||||
def to_activity(self):
|
def to_activity(self):
|
||||||
''' overrides default to manually set serializer '''
|
""" overrides default to manually set serializer """
|
||||||
return activitypub.Follow(**generate_activity(self))
|
return activitypub.Follow(**generate_activity(self))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' really really don't let a user follow someone who blocked them '''
|
""" really really don't let a user follow someone who blocked them """
|
||||||
# blocking in either direction is a no-go
|
# blocking in either direction is a no-go
|
||||||
if UserBlocks.objects.filter(
|
if UserBlocks.objects.filter(
|
||||||
Q(
|
Q(
|
||||||
user_subject=self.user_subject,
|
user_subject=self.user_subject,
|
||||||
user_object=self.user_object,
|
user_object=self.user_object,
|
||||||
) | Q(
|
)
|
||||||
|
| Q(
|
||||||
user_subject=self.user_object,
|
user_subject=self.user_object,
|
||||||
user_object=self.user_subject,
|
user_object=self.user_subject,
|
||||||
)
|
)
|
||||||
|
@ -83,7 +86,7 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, follow_request):
|
def from_request(cls, follow_request):
|
||||||
''' converts a follow request into a follow relationship '''
|
""" converts a follow request into a follow relationship """
|
||||||
return cls.objects.create(
|
return cls.objects.create(
|
||||||
user_subject=follow_request.user_subject,
|
user_subject=follow_request.user_subject,
|
||||||
user_object=follow_request.user_object,
|
user_object=follow_request.user_object,
|
||||||
|
@ -92,12 +95,13 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||||
|
|
||||||
|
|
||||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
''' following a user requires manual or automatic confirmation '''
|
""" following a user requires manual or automatic confirmation """
|
||||||
status = 'follow_request'
|
|
||||||
|
status = "follow_request"
|
||||||
activity_serializer = activitypub.Follow
|
activity_serializer = activitypub.Follow
|
||||||
|
|
||||||
def save(self, *args, broadcast=True, **kwargs):
|
def save(self, *args, broadcast=True, **kwargs):
|
||||||
''' make sure the follow or block relationship doesn't already exist '''
|
""" make sure the follow or block relationship doesn't already exist """
|
||||||
# don't create a request if a follow already exists
|
# don't create a request if a follow already exists
|
||||||
if UserFollows.objects.filter(
|
if UserFollows.objects.filter(
|
||||||
user_subject=self.user_subject,
|
user_subject=self.user_subject,
|
||||||
|
@ -109,7 +113,8 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
Q(
|
Q(
|
||||||
user_subject=self.user_subject,
|
user_subject=self.user_subject,
|
||||||
user_object=self.user_object,
|
user_object=self.user_object,
|
||||||
) | Q(
|
)
|
||||||
|
| Q(
|
||||||
user_subject=self.user_object,
|
user_subject=self.user_object,
|
||||||
user_object=self.user_subject,
|
user_object=self.user_subject,
|
||||||
)
|
)
|
||||||
|
@ -125,39 +130,35 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
if not manually_approves:
|
if not manually_approves:
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
model = apps.get_model('bookwyrm.Notification', require_ready=True)
|
model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||||
notification_type = 'FOLLOW_REQUEST' if \
|
notification_type = "FOLLOW_REQUEST" if manually_approves else "FOLLOW"
|
||||||
manually_approves else 'FOLLOW'
|
|
||||||
model.objects.create(
|
model.objects.create(
|
||||||
user=self.user_object,
|
user=self.user_object,
|
||||||
related_user=self.user_subject,
|
related_user=self.user_subject,
|
||||||
notification_type=notification_type,
|
notification_type=notification_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
''' turn this request into the real deal'''
|
""" turn this request into the real deal"""
|
||||||
user = self.user_object
|
user = self.user_object
|
||||||
if not self.user_subject.local:
|
if not self.user_subject.local:
|
||||||
activity = activitypub.Accept(
|
activity = activitypub.Accept(
|
||||||
id=self.get_remote_id(status='accepts'),
|
id=self.get_remote_id(status="accepts"),
|
||||||
actor=self.user_object.remote_id,
|
actor=self.user_object.remote_id,
|
||||||
object=self.to_activity()
|
object=self.to_activity(),
|
||||||
).serialize()
|
).serialize()
|
||||||
self.broadcast(activity, user)
|
self.broadcast(activity, user)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
UserFollows.from_request(self)
|
UserFollows.from_request(self)
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def reject(self):
|
def reject(self):
|
||||||
''' generate a Reject for this follow request '''
|
""" generate a Reject for this follow request """
|
||||||
if self.user_object.local:
|
if self.user_object.local:
|
||||||
activity = activitypub.Reject(
|
activity = activitypub.Reject(
|
||||||
id=self.get_remote_id(status='rejects'),
|
id=self.get_remote_id(status="rejects"),
|
||||||
actor=self.user_object.remote_id,
|
actor=self.user_object.remote_id,
|
||||||
object=self.to_activity()
|
object=self.to_activity(),
|
||||||
).serialize()
|
).serialize()
|
||||||
self.broadcast(activity, self.user_object)
|
self.broadcast(activity, self.user_object)
|
||||||
|
|
||||||
|
@ -165,19 +166,20 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
|
|
||||||
|
|
||||||
class UserBlocks(ActivityMixin, UserRelationship):
|
class UserBlocks(ActivityMixin, UserRelationship):
|
||||||
''' prevent another user from following you and seeing your posts '''
|
""" prevent another user from following you and seeing your posts """
|
||||||
status = 'blocks'
|
|
||||||
|
status = "blocks"
|
||||||
activity_serializer = activitypub.Block
|
activity_serializer = activitypub.Block
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' remove follow or follow request rels after a block is created '''
|
""" remove follow or follow request rels after a block is created """
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
UserFollows.objects.filter(
|
UserFollows.objects.filter(
|
||||||
Q(user_subject=self.user_subject, user_object=self.user_object) | \
|
Q(user_subject=self.user_subject, user_object=self.user_object)
|
||||||
Q(user_subject=self.user_object, user_object=self.user_subject)
|
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
||||||
).delete()
|
).delete()
|
||||||
UserFollowRequest.objects.filter(
|
UserFollowRequest.objects.filter(
|
||||||
Q(user_subject=self.user_subject, user_object=self.user_object) | \
|
Q(user_subject=self.user_subject, user_object=self.user_object)
|
||||||
Q(user_subject=self.user_object, user_object=self.user_subject)
|
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
||||||
).delete()
|
).delete()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' puttin' books on shelves '''
|
""" puttin' books on shelves """
|
||||||
import re
|
import re
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
@ -9,61 +9,68 @@ from . import fields
|
||||||
|
|
||||||
|
|
||||||
class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
''' a list of books owned by a user '''
|
""" a list of books owned by a user """
|
||||||
|
|
||||||
name = fields.CharField(max_length=100)
|
name = fields.CharField(max_length=100)
|
||||||
identifier = models.CharField(max_length=100)
|
identifier = models.CharField(max_length=100)
|
||||||
user = fields.ForeignKey(
|
user = fields.ForeignKey(
|
||||||
'User', on_delete=models.PROTECT, activitypub_field='owner')
|
"User", on_delete=models.PROTECT, activitypub_field="owner"
|
||||||
|
)
|
||||||
editable = models.BooleanField(default=True)
|
editable = models.BooleanField(default=True)
|
||||||
privacy = fields.PrivacyField()
|
privacy = fields.PrivacyField()
|
||||||
books = models.ManyToManyField(
|
books = models.ManyToManyField(
|
||||||
'Edition',
|
"Edition",
|
||||||
symmetrical=False,
|
symmetrical=False,
|
||||||
through='ShelfBook',
|
through="ShelfBook",
|
||||||
through_fields=('shelf', 'book')
|
through_fields=("shelf", "book"),
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_serializer = activitypub.Shelf
|
activity_serializer = activitypub.Shelf
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
''' set the identifier '''
|
""" set the identifier """
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if not self.identifier:
|
if not self.identifier:
|
||||||
slug = re.sub(r'[^\w]', '', self.name).lower()
|
slug = re.sub(r"[^\w]", "", self.name).lower()
|
||||||
self.identifier = '%s-%d' % (slug, self.id)
|
self.identifier = "%s-%d" % (slug, self.id)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collection_queryset(self):
|
def collection_queryset(self):
|
||||||
''' list of books for this shelf, overrides OrderedCollectionMixin '''
|
""" list of books for this shelf, overrides OrderedCollectionMixin """
|
||||||
return self.books.all().order_by('shelfbook')
|
return self.books.all().order_by("shelfbook")
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
''' shelf identifier instead of id '''
|
""" shelf identifier instead of id """
|
||||||
base_path = self.user.remote_id
|
base_path = self.user.remote_id
|
||||||
return '%s/shelf/%s' % (base_path, self.identifier)
|
return "%s/shelf/%s" % (base_path, self.identifier)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' user/shelf unqiueness '''
|
""" user/shelf unqiueness """
|
||||||
unique_together = ('user', 'identifier')
|
|
||||||
|
unique_together = ("user", "identifier")
|
||||||
|
|
||||||
|
|
||||||
class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
||||||
''' many to many join table for books and shelves '''
|
""" many to many join table for books and shelves """
|
||||||
|
|
||||||
book = fields.ForeignKey(
|
book = fields.ForeignKey(
|
||||||
'Edition', on_delete=models.PROTECT, activitypub_field='object')
|
"Edition", on_delete=models.PROTECT, activitypub_field="object"
|
||||||
|
)
|
||||||
shelf = fields.ForeignKey(
|
shelf = fields.ForeignKey(
|
||||||
'Shelf', on_delete=models.PROTECT, activitypub_field='target')
|
"Shelf", on_delete=models.PROTECT, activitypub_field="target"
|
||||||
|
)
|
||||||
user = fields.ForeignKey(
|
user = fields.ForeignKey(
|
||||||
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
||||||
|
)
|
||||||
|
|
||||||
activity_serializer = activitypub.Add
|
activity_serializer = activitypub.Add
|
||||||
object_field = 'book'
|
object_field = "book"
|
||||||
collection_field = 'shelf'
|
collection_field = "shelf"
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' an opinionated constraint!
|
"""an opinionated constraint!
|
||||||
you can't put a book on shelf twice '''
|
you can't put a book on shelf twice"""
|
||||||
unique_together = ('book', 'shelf')
|
|
||||||
ordering = ('-created_date',)
|
unique_together = ("book", "shelf")
|
||||||
|
ordering = ("-created_date",)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' the particulars for this instance of BookWyrm '''
|
""" the particulars for this instance of BookWyrm """
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
@ -9,36 +9,31 @@ from django.utils import timezone
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
class SiteSettings(models.Model):
|
class SiteSettings(models.Model):
|
||||||
''' customized settings for this instance '''
|
""" customized settings for this instance """
|
||||||
name = models.CharField(default='BookWyrm', max_length=100)
|
|
||||||
|
name = models.CharField(default="BookWyrm", max_length=100)
|
||||||
instance_tagline = models.CharField(
|
instance_tagline = models.CharField(
|
||||||
max_length=150, default='Social Reading and Reviewing')
|
max_length=150, default="Social Reading and Reviewing"
|
||||||
instance_description = models.TextField(
|
)
|
||||||
default='This instance has no description.')
|
instance_description = models.TextField(default="This instance has no description.")
|
||||||
registration_closed_text = models.TextField(
|
registration_closed_text = models.TextField(
|
||||||
default='Contact an administrator to get an invite')
|
default="Contact an administrator to get an invite"
|
||||||
code_of_conduct = models.TextField(
|
)
|
||||||
default='Add a code of conduct here.')
|
code_of_conduct = models.TextField(default="Add a code of conduct here.")
|
||||||
privacy_policy = models.TextField(
|
privacy_policy = models.TextField(default="Add a privacy policy here.")
|
||||||
default='Add a privacy policy here.')
|
|
||||||
allow_registration = models.BooleanField(default=True)
|
allow_registration = models.BooleanField(default=True)
|
||||||
logo = models.ImageField(
|
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||||
upload_to='logos/', null=True, blank=True
|
logo_small = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||||
)
|
favicon = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||||
logo_small = models.ImageField(
|
|
||||||
upload_to='logos/', null=True, blank=True
|
|
||||||
)
|
|
||||||
favicon = models.ImageField(
|
|
||||||
upload_to='logos/', null=True, blank=True
|
|
||||||
)
|
|
||||||
support_link = models.CharField(max_length=255, null=True, blank=True)
|
support_link = models.CharField(max_length=255, null=True, blank=True)
|
||||||
support_title = models.CharField(max_length=100, 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)
|
admin_email = models.EmailField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls):
|
def get(cls):
|
||||||
''' gets the site settings db entry or defaults '''
|
""" gets the site settings db entry or defaults """
|
||||||
try:
|
try:
|
||||||
return cls.objects.get(id=1)
|
return cls.objects.get(id=1)
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
|
@ -46,12 +41,15 @@ class SiteSettings(models.Model):
|
||||||
default_settings.save()
|
default_settings.save()
|
||||||
return default_settings
|
return default_settings
|
||||||
|
|
||||||
|
|
||||||
def new_access_code():
|
def new_access_code():
|
||||||
''' the identifier for a user invite '''
|
""" the identifier for a user invite """
|
||||||
return base64.b32encode(Random.get_random_bytes(5)).decode('ascii')
|
return base64.b32encode(Random.get_random_bytes(5)).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
class SiteInvite(models.Model):
|
class SiteInvite(models.Model):
|
||||||
''' gives someone access to create an account on the instance '''
|
""" gives someone access to create an account on the instance """
|
||||||
|
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
code = models.CharField(max_length=32, default=new_access_code)
|
code = models.CharField(max_length=32, default=new_access_code)
|
||||||
expiry = models.DateTimeField(blank=True, null=True)
|
expiry = models.DateTimeField(blank=True, null=True)
|
||||||
|
@ -60,34 +58,35 @@ class SiteInvite(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
def valid(self):
|
def valid(self):
|
||||||
''' make sure it hasn't expired or been used '''
|
""" make sure it hasn't expired or been used """
|
||||||
return (
|
return (self.expiry is None or self.expiry > timezone.now()) and (
|
||||||
(self.expiry is None or self.expiry > timezone.now()) and
|
self.use_limit is None or self.times_used < self.use_limit
|
||||||
(self.use_limit is None or self.times_used < self.use_limit))
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
''' formats the invite link '''
|
""" formats the invite link """
|
||||||
return 'https://{}/invite/{}'.format(DOMAIN, self.code)
|
return "https://{}/invite/{}".format(DOMAIN, self.code)
|
||||||
|
|
||||||
|
|
||||||
def get_passowrd_reset_expiry():
|
def get_passowrd_reset_expiry():
|
||||||
''' give people a limited time to use the link '''
|
""" give people a limited time to use the link """
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
return now + datetime.timedelta(days=1)
|
return now + datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
class PasswordReset(models.Model):
|
class PasswordReset(models.Model):
|
||||||
''' gives someone access to create an account on the instance '''
|
""" gives someone access to create an account on the instance """
|
||||||
|
|
||||||
code = models.CharField(max_length=32, default=new_access_code)
|
code = models.CharField(max_length=32, default=new_access_code)
|
||||||
expiry = models.DateTimeField(default=get_passowrd_reset_expiry)
|
expiry = models.DateTimeField(default=get_passowrd_reset_expiry)
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
def valid(self):
|
def valid(self):
|
||||||
''' make sure it hasn't expired or been used '''
|
""" make sure it hasn't expired or been used """
|
||||||
return self.expiry > timezone.now()
|
return self.expiry > timezone.now()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
''' formats the invite link '''
|
""" formats the invite link """
|
||||||
return 'https://{}/password-reset/{}'.format(DOMAIN, self.code)
|
return "https://{}/password-reset/{}".format(DOMAIN, self.code)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue