mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-01 14:01:35 +00:00
Merge branch 'main' into ci
This commit is contained in:
commit
b9a61162d7
292 changed files with 18523 additions and 10503 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"
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,6 +2,7 @@
|
||||||
/venv
|
/venv
|
||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
|
**/__pycache__
|
||||||
|
|
||||||
# VSCode
|
# VSCode
|
||||||
/.vscode
|
/.vscode
|
||||||
|
|
11
Dockerfile
11
Dockerfile
|
@ -2,15 +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
|
RUN apt-get update && apt-get install -y gettext libgettextpo-dev && apt-get clean
|
||||||
|
|
||||||
COPY ./bookwyrm /app
|
COPY ./bookwyrm ./celerywyrm /app/
|
||||||
COPY ./celerywyrm /app
|
|
||||||
|
|
124
README.md
124
README.md
|
@ -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
|
||||||
```
|
```
|
||||||
|
@ -173,6 +176,28 @@ Instructions for running BookWyrm in production:
|
||||||
- Run docker-compose in the background with: `docker-compose up -d`
|
- Run docker-compose in the background with: `docker-compose up -d`
|
||||||
- Initialize the database with: `./bw-dev initdb`
|
- Initialize the database with: `./bw-dev initdb`
|
||||||
- Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U <databasename>` and saves the backup to a safe location
|
- Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U <databasename>` and saves the backup to a safe location
|
||||||
|
- Get the application code:
|
||||||
|
`git clone git@github.com:mouse-reeve/bookwyrm.git`
|
||||||
|
- Switch to the `production` branch
|
||||||
|
`git checkout production`
|
||||||
|
- Create your environment variables file
|
||||||
|
`cp .env.example .env`
|
||||||
|
- Add your domain, email address, SMTP credentials
|
||||||
|
- Set a secure redis password and secret key
|
||||||
|
- Set a secure database password for postgres
|
||||||
|
- Update your nginx configuration in `nginx/default.conf`
|
||||||
|
- Replace `your-domain.com` with your domain name
|
||||||
|
- If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`.
|
||||||
|
- If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy)
|
||||||
|
- Run the application (this should also set up a Certbot ssl cert for your domain) with
|
||||||
|
`docker-compose up --build`, and make sure all the images build successfully
|
||||||
|
- If you are running other services on your host machine, you may run into errors where services fail when attempting to bind to a port.
|
||||||
|
See the [troubleshooting guide](#port-conflicts) for advice on resolving this.
|
||||||
|
- When docker has built successfully, stop the process with `CTRL-C`
|
||||||
|
- Comment out the `command: certonly...` line in `docker-compose.yml`, and uncomment the following line (`command: renew ...`) so that the certificate will be automatically renewed.
|
||||||
|
- Uncomment the https redirect and `server` block in `nginx/default.conf` (lines 17-48).
|
||||||
|
- Run docker-compose in the background with: `docker-compose up -d`
|
||||||
|
- Initialize the database with: `./bw-dev initdb`
|
||||||
|
|
||||||
Congrats! You did it, go to your domain and enjoy the fruits of your labors.
|
Congrats! You did it, go to your domain and enjoy the fruits of your labors.
|
||||||
|
|
||||||
|
@ -202,3 +227,96 @@ There are three concepts in the book data model:
|
||||||
|
|
||||||
Whenever a user interacts with a book, they are interacting with a specific edition. Every work has a default edition, but the user can select other editions. Reviews aggregated for all editions of a work when you view an edition's page.
|
Whenever a user interacts with a book, they are interacting with a specific edition. Every work has a default edition, but the user can select other editions. Reviews aggregated for all editions of a work when you view an edition's page.
|
||||||
|
|
||||||
|
### Backups
|
||||||
|
|
||||||
|
BookWyrm's db service dumps a backup copy of its database to its `/backups` directory daily at midnight UTC.
|
||||||
|
Backups are named `backup__%Y-%m-%d.sql`.
|
||||||
|
|
||||||
|
The db service has an optional script for periodically pruning the backups directory so that all recent daily backups are kept, but for older backups, only weekly or monthly backups are kept.
|
||||||
|
To enable this script:
|
||||||
|
- Uncomment the final line in `postgres-docker/cronfile`
|
||||||
|
- rebuild your instance `docker-compose up --build`
|
||||||
|
|
||||||
|
You can copy backups from the backups volume to your host machine with `docker cp`:
|
||||||
|
- Run `docker-compose ps` to confirm the db service's full name (it's probably `bookwyrm_db_1`.
|
||||||
|
- Run `docker cp <container_name>:/backups <host machine path>`
|
||||||
|
|
||||||
|
### Port Conflicts
|
||||||
|
|
||||||
|
BookWyrm has multiple services that run on their default ports.
|
||||||
|
This means that, depending on what else you are running on your host machine, you may run into errors when building or running BookWyrm when attempts to bind to those ports fail.
|
||||||
|
|
||||||
|
If this occurs, you will need to change your configuration to run services on different ports.
|
||||||
|
This may require one or more changes the following files:
|
||||||
|
- `docker-compose.yml`
|
||||||
|
- `nginx/default.conf`
|
||||||
|
- `.env` (You create this file yourself during setup)
|
||||||
|
|
||||||
|
E.g., If you need Redis to run on a different port:
|
||||||
|
- In `docker-compose.yml`:
|
||||||
|
- In `services` -> `redis` -> `command`, add `--port YOUR_PORT` to the command
|
||||||
|
- In `services` -> `redis` -> `ports`, change `6379:6379` to your port
|
||||||
|
- In `.env`, update `REDIS_PORT`
|
||||||
|
|
||||||
|
If you are already running a web-server on your machine, you will need to set up a reverse-proxy.
|
||||||
|
|
||||||
|
#### Running BookWyrm Behind a Reverse-Proxy
|
||||||
|
|
||||||
|
If you are running another web-server on your machine, you should have it handle proxying web requests to BookWyrm.
|
||||||
|
|
||||||
|
The default BookWyrm configuration already has an nginx server that proxies requests to the django app that handles SSL and directly serves static files.
|
||||||
|
The static files are stored in a Docker volume that several BookWyrm services access, so it is not recommended to remove this server completely.
|
||||||
|
|
||||||
|
To run BookWyrm behind a reverse-proxy, make the following changes:
|
||||||
|
- In `nginx/default.conf`:
|
||||||
|
- Comment out the two default servers
|
||||||
|
- Uncomment the server labeled Reverse-Proxy server
|
||||||
|
- Replace `your-domain.com` with your domain name
|
||||||
|
- In `docker-compose.yml`:
|
||||||
|
- In `services` -> `nginx` -> `ports`, comment out the default ports and add `- 8001:8001`
|
||||||
|
- In `services` -> `nginx` -> `volumes`, comment out the two volumes that begin `./certbot/`
|
||||||
|
- In `services`, comment out the `certbot` service
|
||||||
|
|
||||||
|
At this point, you can follow, the [setup](#server-setup) instructions as listed.
|
||||||
|
Once docker is running, you can access your BookWyrm instance at `http://localhost:8001` (**NOTE:** your server is not accessible over `https`).
|
||||||
|
|
||||||
|
Steps for setting up a reverse-proxy are server dependent.
|
||||||
|
|
||||||
|
##### Nginx
|
||||||
|
|
||||||
|
Before you can set up nginx, you will need to locate your nginx configuration directory, which is dependent on your platform and how you installed nginx.
|
||||||
|
See [nginx's guide](http://nginx.org/en/docs/beginners_guide.html) for details.
|
||||||
|
|
||||||
|
To set up your server:
|
||||||
|
- In you `nginx.conf` file, ensure that `include servers/*;` isn't commented out.
|
||||||
|
- In your nginx `servers` directory, create a new file named after your domain containing the following information:
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
server_name your-domain.com www.your-domain.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8000;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /images/ {
|
||||||
|
proxy_pass http://localhost:8001;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
proxy_pass http://localhost:8001;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen [::]:80 ssl;
|
||||||
|
listen 80 ssl;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- run `sudo certbot run --nginx --email YOUR_EMAIL -d your-domain.com -d www.your-domain.com`
|
||||||
|
- restart nginx
|
||||||
|
|
||||||
|
If everything worked correctly, your BookWyrm instance should now be externally accessible.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' bring activitypub functions into the namespace '''
|
""" bring activitypub functions into the namespace """
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ from .base_activity import ActivityEncoder, Signature, naive_parse
|
||||||
from .base_activity import Link, Mention
|
from .base_activity import Link, Mention
|
||||||
from .base_activity import ActivitySerializerError, resolve_remote_id
|
from .base_activity import ActivitySerializerError, resolve_remote_id
|
||||||
from .image import Image
|
from .image import Image
|
||||||
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
|
from .note import Note, GeneratedNote, Article, Comment, Quotation
|
||||||
|
from .note import Review, Rating
|
||||||
from .note import Tombstone
|
from .note import Tombstone
|
||||||
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
||||||
from .ordered_collection import BookList, Shelf
|
from .ordered_collection import BookList, Shelf
|
||||||
|
@ -21,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,25 +63,26 @@ 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]
|
||||||
if value in (None, MISSING):
|
if value in (None, MISSING, {}):
|
||||||
raise KeyError()
|
raise KeyError()
|
||||||
try:
|
try:
|
||||||
is_subclass = issubclass(field.type, ActivityObject)
|
is_subclass = issubclass(field.type, 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():
|
||||||
|
@ -176,22 +186,19 @@ class ActivityObject:
|
||||||
data[k] = v.serialize()
|
data[k] = v.serialize()
|
||||||
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,43 +212,45 @@ 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:
|
||||||
return result
|
return result
|
||||||
|
@ -249,13 +258,13 @@ def resolve_remote_id(remote_id, model=None, refresh=False, save=True):
|
||||||
# load the data and create the object
|
# load the data and create the object
|
||||||
try:
|
try:
|
||||||
data = get_data(remote_id)
|
data = get_data(remote_id)
|
||||||
except (ConnectorException, ConnectionError):
|
except ConnectorException:
|
||||||
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" % (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 = "Document"
|
||||||
id: str = ''
|
id: str = None
|
||||||
|
|
|
@ -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,64 +6,81 @@ 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'
|
|
||||||
|
|
||||||
def to_model(self, *args, **kwargs):
|
type: str = "Tombstone"
|
||||||
''' this should never really get serialized, just searched for '''
|
|
||||||
model = apps.get_model('bookwyrm.Status')
|
def to_model(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||||
|
""" this should never really get serialized, just searched for """
|
||||||
|
model = apps.get_model("bookwyrm.Status")
|
||||||
return model.find_existing_by_remote_id(self.id)
|
return model.find_existing_by_remote_id(self.id)
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
class Review(Comment):
|
|
||||||
''' a full book review '''
|
|
||||||
name: str = None
|
|
||||||
rating: int = None
|
|
||||||
type: str = 'Review'
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
||||||
|
class Review(Comment):
|
||||||
|
""" a full book review """
|
||||||
|
|
||||||
|
name: str = None
|
||||||
|
rating: int = None
|
||||||
|
type: str = "Review"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(init=False)
|
||||||
|
class Rating(Comment):
|
||||||
|
""" just a star rating """
|
||||||
|
|
||||||
|
rating: int
|
||||||
|
content: str = None
|
||||||
|
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,25 +8,27 @@ 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
|
||||||
followers: str
|
followers: str
|
||||||
publicKey: PublicKey
|
publicKey: PublicKey
|
||||||
endpoints: Dict
|
endpoints: Dict = None
|
||||||
name: str = None
|
name: str = None
|
||||||
summary: str = None
|
summary: str = None
|
||||||
icon: Image = field(default_factory=lambda: {})
|
icon: Image = field(default_factory=lambda: {})
|
||||||
bookwyrmUser: bool = False
|
bookwyrmUser: bool = False
|
||||||
manuallyApprovesFollowers: str = False
|
manuallyApprovesFollowers: str = False
|
||||||
discoverable: str = True
|
discoverable: str = True
|
||||||
type: str = 'Person'
|
type: str = "Person"
|
||||||
|
|
|
@ -9,10 +9,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,191 @@ 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 """
|
||||||
|
if isinstance(self.object, str):
|
||||||
|
# it may be that sometihng should be done with these, but idk what
|
||||||
|
# this seems just to be coming from pleroma
|
||||||
|
return
|
||||||
|
|
||||||
# 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)
|
||||||
|
if not obj:
|
||||||
|
# this could be a folloq request not a follow proper
|
||||||
|
model = apps.get_model("bookwyrm.UserFollowRequest")
|
||||||
|
obj = self.object.to_model(model=model, save=False, allow_create=False)
|
||||||
|
else:
|
||||||
|
obj = self.object.to_model(model=model, save=False, allow_create=False)
|
||||||
|
if not obj:
|
||||||
|
# if we don't have the object, we can't undo it. happens a lot with boosts
|
||||||
|
return
|
||||||
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 get the 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)
|
target = resolve_remote_id(self.target, refresh=False)
|
||||||
|
model = [t for t in type(target)._meta.related_objects if t.name != "edition"][
|
||||||
|
0
|
||||||
|
].related_model
|
||||||
|
obj = self.to_model(model=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,82 +25,94 @@ 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(
|
data = get_data(
|
||||||
'%s%s' % (self.search_url, query),
|
"%s%s" % (self.search_url, query),
|
||||||
params=params,
|
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 = []
|
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 = {}
|
||||||
|
data = get_data(
|
||||||
|
"%s%s" % (self.isbn_search_url, query),
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# this shouldn't be returning mutliple results, but just in case
|
||||||
|
for doc in self.parse_isbn_search_data(data)[:10]:
|
||||||
|
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 +136,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 +150,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 +170,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,48 +183,48 @@ 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)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_data(url):
|
def get_data(url, params=None):
|
||||||
''' wrapper for request.get '''
|
""" wrapper for request.get """
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
url,
|
url,
|
||||||
|
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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except (RequestError, SSLError) as e:
|
except (RequestError, SSLError, ConnectionError) as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
raise ConnectorException()
|
raise ConnectorException()
|
||||||
|
|
||||||
|
@ -227,12 +240,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,27 +258,31 @@ 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
|
|
||||||
year: str
|
|
||||||
connector: object
|
connector: object
|
||||||
|
author: str = None
|
||||||
|
year: str = None
|
||||||
|
cover: str = None
|
||||||
confidence: int = 1
|
confidence: int = 1
|
||||||
|
|
||||||
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,11 +291,11 @@ 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
|
||||||
try:
|
try:
|
||||||
return self.formatter(value)
|
return self.formatter(value)
|
||||||
except:# pylint: disable=bare-except
|
except: # pylint: disable=bare-except
|
||||||
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,11 @@ 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)
|
return SearchResult(**search_result)
|
||||||
|
|
||||||
|
def parse_isbn_search_data(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
return self.format_search_result(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(r"[\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():
|
||||||
try:
|
result_set = None
|
||||||
result_set = connector.search(query, min_confidence=min_confidence)
|
if maybe_isbn:
|
||||||
except (HTTPError, ConnectorException):
|
# Search on ISBN
|
||||||
continue
|
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
|
||||||
|
|
||||||
result_set = [r for r in result_set \
|
# if no isbn search or results, we fallback to generic search
|
||||||
if dedup_slug(r) not in result_index]
|
if result_set in (None, []):
|
||||||
|
try:
|
||||||
|
result_set = connector.search(query, min_confidence=min_confidence)
|
||||||
|
except (HTTPError, ConnectorException):
|
||||||
|
continue
|
||||||
|
|
||||||
|
result_set = [r for r in result_set 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,139 @@ 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, size="L"):
|
||||||
def get_cover_url(self, cover_blob):
|
""" ask openlibrary for the cover """
|
||||||
''' ask openlibrary for the cover '''
|
if not cover_blob:
|
||||||
|
return None
|
||||||
cover_id = cover_blob[0]
|
cover_id = cover_blob[0]
|
||||||
image_name = '%s-L.jpg' % cover_id
|
image_name = "%s-%s.jpg" % (cover_id, size)
|
||||||
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"]
|
||||||
|
cover_blob = search_result.get("cover_i")
|
||||||
|
cover = self.get_cover_url([cover_blob], size="M") if cover_blob else None
|
||||||
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"),
|
||||||
|
cover=cover,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 +155,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 +163,59 @@ 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'))
|
|
||||||
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'))
|
|
||||||
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'))
|
|
||||||
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'))
|
):
|
||||||
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,45 @@ 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 \
|
cover="%s%s" % (self.covers_url, search_result.cover),
|
||||||
hasattr(search_result, 'rank') else 1,
|
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
return self.format_search_result(search_result)
|
||||||
|
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
pass
|
pass
|
||||||
|
@ -59,8 +86,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 +99,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
|
|
||||||
''' include the custom info about the site '''
|
def site_settings(request): # pylint: disable=unused-argument
|
||||||
return {
|
""" include the custom info about the site """
|
||||||
'site': models.SiteSettings.objects.get()
|
return {"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
|
||||||
|
|
||||||
|
@ -12,99 +12,116 @@ 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.Review
|
model = models.ReviewRating
|
||||||
fields = ['user', 'book', 'content', '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",
|
||||||
|
"show_goal",
|
||||||
]
|
]
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
@ -112,15 +129,15 @@ class EditUserForm(CustomForm):
|
||||||
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}
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,80 +145,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",
|
||||||
'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, _("%(count)d uses" % {'count': 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):
|
||||||
|
@ -213,4 +237,10 @@ 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"]
|
||||||
|
|
||||||
|
|
||||||
|
class ReportForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Report
|
||||||
|
fields = ["user", "reporter", "statuses", "note"]
|
||||||
|
|
|
@ -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,15 +57,15 @@ 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():
|
||||||
try:
|
try:
|
||||||
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,41 +85,41 @@ 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,
|
||||||
start_date=read.start_date,
|
book=item.book,
|
||||||
finish_date=read.finish_date
|
start_date=read.start_date,
|
||||||
).exists():
|
finish_date=read.finish_date,
|
||||||
|
).exists():
|
||||||
continue
|
continue
|
||||||
read.book = item.book
|
read.book = item.book
|
||||||
read.user = user
|
read.user = user
|
||||||
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 = (
|
||||||
item.book.title,
|
"Review of {!r} on {!r}".format(
|
||||||
source,
|
item.book.title,
|
||||||
) if item.review else ''
|
source,
|
||||||
|
)
|
||||||
|
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/" % 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/",
|
||||||
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,
|
||||||
# mustn't be the only edition for the work
|
**null_fields
|
||||||
parent_work__editions__count__gt=1
|
)
|
||||||
|
.annotate(Count("parent_work__editions"))
|
||||||
|
.filter(
|
||||||
|
# mustn't be the only edition for the work
|
||||||
|
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",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
66
bookwyrm/migrations/0046_reviewrating.py
Normal file
66
bookwyrm/migrations/0046_reviewrating.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-02-25 18:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db import connection
|
||||||
|
from django.db.models import Q
|
||||||
|
import django.db.models.deletion
|
||||||
|
from psycopg2.extras import execute_values
|
||||||
|
|
||||||
|
|
||||||
|
def convert_review_rating(app_registry, schema_editor):
|
||||||
|
""" take rating type Reviews and convert them to ReviewRatings """
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
reviews = (
|
||||||
|
app_registry.get_model("bookwyrm", "Review")
|
||||||
|
.objects.using(db_alias)
|
||||||
|
.filter(Q(content__isnull=True) | Q(content=""))
|
||||||
|
)
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
values = [(r.id,) for r in reviews]
|
||||||
|
execute_values(
|
||||||
|
cursor,
|
||||||
|
"""
|
||||||
|
INSERT INTO bookwyrm_reviewrating(review_ptr_id)
|
||||||
|
VALUES %s""",
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unconvert_review_rating(app_registry, schema_editor):
|
||||||
|
""" undo the conversion from ratings back to reviews"""
|
||||||
|
# All we need to do to revert this is drop the table, which Django will do
|
||||||
|
# on its own, as long as we have a valid reverse function. So, this is a
|
||||||
|
# no-op function so Django will do its thing
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0045_auto_20210210_2114"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ReviewRating",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"review_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="bookwyrm.Review",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=("bookwyrm.review",),
|
||||||
|
),
|
||||||
|
migrations.RunPython(convert_review_rating, unconvert_review_rating),
|
||||||
|
]
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
13
bookwyrm/migrations/0047_merge_20210228_1839.py
Normal file
13
bookwyrm/migrations/0047_merge_20210228_1839.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-02-28 18:39
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0046_reviewrating"),
|
||||||
|
("bookwyrm", "0046_sitesettings_privacy_policy"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
13
bookwyrm/migrations/0048_merge_20210308_1754.py
Normal file
13
bookwyrm/migrations/0048_merge_20210308_1754.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-03-08 17:54
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0047_connector_isbn_search_url"),
|
||||||
|
("bookwyrm", "0047_merge_20210228_1839"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
113
bookwyrm/migrations/0049_auto_20210309_0156.py
Normal file
113
bookwyrm/migrations/0049_auto_20210309_0156.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-03-09 01:56
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.db.models.expressions
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0048_merge_20210308_1754"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Report",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_date", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"remote_id",
|
||||||
|
bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("note", models.TextField(blank=True, null=True)),
|
||||||
|
("resolved", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"reporter",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="reporter",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"statuses",
|
||||||
|
models.ManyToManyField(blank=True, null=True, to="bookwyrm.Status"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ReportComment",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_date", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"remote_id",
|
||||||
|
bookwyrm.models.fields.RemoteIdField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("note", models.TextField()),
|
||||||
|
(
|
||||||
|
"report",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.Report",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="report",
|
||||||
|
constraint=models.CheckConstraint(
|
||||||
|
check=models.Q(
|
||||||
|
_negated=True, reporter=django.db.models.expressions.F("user")
|
||||||
|
),
|
||||||
|
name="self_report",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
26
bookwyrm/migrations/0050_auto_20210313_0030.py
Normal file
26
bookwyrm/migrations/0050_auto_20210313_0030.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-03-13 00:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0049_auto_20210309_0156"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="report",
|
||||||
|
options={"ordering": ("-created_date",)},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="reportcomment",
|
||||||
|
options={"ordering": ("-created_date",)},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="report",
|
||||||
|
name="statuses",
|
||||||
|
field=models.ManyToManyField(blank=True, to="bookwyrm.Status"),
|
||||||
|
),
|
||||||
|
]
|
66
bookwyrm/migrations/0051_auto_20210316_1950.py
Normal file
66
bookwyrm/migrations/0051_auto_20210316_1950.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-03-16 19:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0050_auto_20210313_0030"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="notification",
|
||||||
|
name="notification_type_valid",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="notification",
|
||||||
|
name="related_report",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="bookwyrm.Report",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="notification",
|
||||||
|
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"),
|
||||||
|
("REPORT", "Report"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="notification",
|
||||||
|
constraint=models.CheckConstraint(
|
||||||
|
check=models.Q(
|
||||||
|
notification_type__in=[
|
||||||
|
"FAVORITE",
|
||||||
|
"REPLY",
|
||||||
|
"MENTION",
|
||||||
|
"TAG",
|
||||||
|
"FOLLOW",
|
||||||
|
"FOLLOW_REQUEST",
|
||||||
|
"BOOST",
|
||||||
|
"IMPORT",
|
||||||
|
"ADD",
|
||||||
|
"REPORT",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
name="notification_type_valid",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
bookwyrm/migrations/0052_user_show_goal.py
Normal file
18
bookwyrm/migrations/0052_user_show_goal.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.7 on 2021-03-18 15:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0051_auto_20210316_1950"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="show_goal",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
30
bookwyrm/migrations/0053_auto_20210319_1913.py
Normal file
30
bookwyrm/migrations/0053_auto_20210319_1913.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 3.1.6 on 2021-03-19 19:13
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0052_user_show_goal"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="review",
|
||||||
|
name="rating",
|
||||||
|
field=bookwyrm.models.fields.DecimalField(
|
||||||
|
blank=True,
|
||||||
|
decimal_places=2,
|
||||||
|
default=None,
|
||||||
|
max_digits=3,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
25
bookwyrm/migrations/0054_auto_20210319_1942.py
Normal file
25
bookwyrm/migrations/0054_auto_20210319_1942.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 3.1.6 on 2021-03-19 19:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0053_auto_20210319_1913"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="importitem",
|
||||||
|
name="data",
|
||||||
|
field=models.JSONField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="first_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
||||||
|
@ -9,7 +9,8 @@ from .connector import Connector
|
||||||
from .shelf import Shelf, ShelfBook
|
from .shelf import Shelf, ShelfBook
|
||||||
from .list import List, ListItem
|
from .list import List, ListItem
|
||||||
|
|
||||||
from .status import Status, GeneratedNote, Review, Comment, Quotation
|
from .status import Status, GeneratedNote, Comment, Quotation
|
||||||
|
from .status import Review, ReviewRating
|
||||||
from .status import Boost
|
from .status import Boost
|
||||||
from .attachment import Image
|
from .attachment import Image
|
||||||
from .favorite import Favorite
|
from .favorite import Favorite
|
||||||
|
@ -20,6 +21,7 @@ from .tag import Tag, UserTag
|
||||||
|
|
||||||
from .user import User, KeyPair, AnnualGoal
|
from .user import User, KeyPair, AnnualGoal
|
||||||
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
||||||
|
from .report import Report, ReportComment
|
||||||
from .federated_server import FederatedServer
|
from .federated_server import FederatedServer
|
||||||
|
|
||||||
from .import_job import ImportJob, ImportItem
|
from .import_job import ImportJob, ImportItem
|
||||||
|
@ -27,8 +29,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,113 +358,120 @@ 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)
|
if self.user.local:
|
||||||
|
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=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=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()
|
# if it's a one-to-one (key pair)
|
||||||
|
if hasattr(related_field, "field_to_activity"):
|
||||||
|
return related_field.field_to_activity()
|
||||||
|
# if it's one-to-many (attachments)
|
||||||
|
return related_field.to_activity()
|
||||||
return related_field.remote_id
|
return related_field.remote_id
|
||||||
|
|
||||||
|
|
||||||
@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, requests.exceptions.ConnectionError):
|
||||||
logger.exception(e)
|
pass
|
||||||
|
|
||||||
|
|
||||||
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 +479,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 +493,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 +506,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,29 @@ 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/",
|
||||||
caption = fields.TextField(null=True, blank=True, activitypub_field='name')
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
activitypub_field="url",
|
||||||
|
alt_field="caption",
|
||||||
|
)
|
||||||
|
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,7 +1,7 @@
|
||||||
''' 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, transaction
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
|
@ -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,44 @@ 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,
|
||||||
|
", ".join(self.publishers) if hasattr(self, "publishers") 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,77 +127,96 @@ 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()
|
@transaction.atomic()
|
||||||
|
def reset_default_edition(self):
|
||||||
|
""" sets a new default edition based on computed rank """
|
||||||
|
self.default_edition = None
|
||||||
|
# editions are re-ranked implicitly
|
||||||
|
self.save()
|
||||||
|
self.default_edition = self.get_default_edition()
|
||||||
|
self.save()
|
||||||
|
|
||||||
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, ignore_default=False):
|
||||||
''' 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 (
|
||||||
|
not ignore_default
|
||||||
|
and 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
|
||||||
rank = 0
|
rank = 0
|
||||||
|
@ -200,9 +231,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 +245,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 +267,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 +284,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,29 +1,30 @@
|
||||||
''' 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)
|
||||||
# how many queries executed in a unit of time, like a day
|
# how many queries executed in a unit of time, like a day
|
||||||
query_count = models.IntegerField(default=0)
|
query_count = models.IntegerField(default=0)
|
||||||
|
@ -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",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue