forked from mirrors/bookwyrm
Merge branch 'main' into list-status
This commit is contained in:
commit
ea0e54e8da
447 changed files with 34575 additions and 14663 deletions
|
@ -1,2 +1,2 @@
|
|||
[run]
|
||||
omit = */test*,celerywyrm*,bookwyrm/migrations/*
|
||||
omit = */test*,celerywyrm*,bookwyrm/migrations/*
|
||||
|
|
|
@ -4,4 +4,4 @@ __pycache__
|
|||
*.pyd
|
||||
.git
|
||||
.github
|
||||
.pytest*
|
||||
.pytest*
|
||||
|
|
39
.editorconfig
Normal file
39
.editorconfig
Normal file
|
@ -0,0 +1,39 @@
|
|||
# @see https://editorconfig.org/
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
|
||||
# C-style doc comments
|
||||
block_comment_start = /*
|
||||
block_comment = *
|
||||
block_comment_end = */
|
||||
|
||||
[{bw-dev,fr-dev,LICENSE}]
|
||||
max_line_length = off
|
||||
|
||||
[*.{csv,json,html,md,po,py,svg,tsv}]
|
||||
max_line_length = off
|
||||
|
||||
# ` ` at the end of a line is a line-break in markdown
|
||||
[*.{md,markdown}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
max_line_length = off
|
||||
|
||||
# Computer generated files
|
||||
[{package.json,*.lock,*.mo}]
|
||||
indent_size = unset
|
||||
indent_style = unset
|
||||
max_line_length = unset
|
||||
insert_final_newline = unset
|
|
@ -5,6 +5,7 @@ SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr"
|
|||
DEBUG=true
|
||||
|
||||
DOMAIN=your.domain.here
|
||||
#EMAIL=your@email.here
|
||||
|
||||
## Leave unset to allow all hosts
|
||||
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
||||
|
@ -22,11 +23,28 @@ POSTGRES_USER=fedireads
|
|||
POSTGRES_DB=fedireads
|
||||
POSTGRES_HOST=db
|
||||
|
||||
CELERY_BROKER=redis://redis:6379/0
|
||||
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
# Redis activity stream manager
|
||||
MAX_STREAM_LENGTH=200
|
||||
REDIS_ACTIVITY_HOST=redis_activity
|
||||
REDIS_ACTIVITY_PORT=6379
|
||||
#REDIS_ACTIVITY_PASSWORD=redispassword345
|
||||
|
||||
# Redis as celery broker
|
||||
#REDIS_BROKER_PORT=6379
|
||||
#REDIS_BROKER_PASSWORD=redispassword123
|
||||
CELERY_BROKER=redis://redis_broker:6379/0
|
||||
CELERY_RESULT_BACKEND=redis://redis_broker:6379/0
|
||||
|
||||
FLOWER_PORT=8888
|
||||
#FLOWER_USER=mouse
|
||||
#FLOWER_PASSWORD=changeme
|
||||
|
||||
EMAIL_HOST="smtp.mailgun.org"
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=mail@your.domain.here
|
||||
EMAIL_HOST_PASSWORD=emailpassword123
|
||||
EMAIL_USE_TLS=true
|
||||
EMAIL_USE_SSL=false
|
||||
|
||||
# Set this to true when initializing certbot for domain, false when not
|
||||
CERTBOT_INIT=false
|
50
.env.prod.example
Normal file
50
.env.prod.example
Normal file
|
@ -0,0 +1,50 @@
|
|||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG=false
|
||||
|
||||
DOMAIN=your.domain.here
|
||||
EMAIL=your@email.here
|
||||
|
||||
## Leave unset to allow all hosts
|
||||
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
||||
|
||||
OL_URL=https://openlibrary.org
|
||||
|
||||
## Database backend to use.
|
||||
## Default is postgres, sqlite is for dev quickstart only (NOT production!!!)
|
||||
BOOKWYRM_DATABASE_BACKEND=postgres
|
||||
|
||||
MEDIA_ROOT=images/
|
||||
|
||||
POSTGRES_PASSWORD=securedbpassword123
|
||||
POSTGRES_USER=fedireads
|
||||
POSTGRES_DB=fedireads
|
||||
POSTGRES_HOST=db
|
||||
|
||||
# Redis activity stream manager
|
||||
MAX_STREAM_LENGTH=200
|
||||
REDIS_ACTIVITY_HOST=redis_activity
|
||||
REDIS_ACTIVITY_PORT=6379
|
||||
REDIS_ACTIVITY_PASSWORD=redispassword345
|
||||
|
||||
# Redis as celery broker
|
||||
REDIS_BROKER_PORT=6379
|
||||
REDIS_BROKER_PASSWORD=redispassword123
|
||||
CELERY_BROKER=redis://:${REDIS_BROKER_PASSWORD}@redis_broker:${REDIS_BROKER_PORT}/0
|
||||
CELERY_RESULT_BACKEND=redis://:${REDIS_BROKER_PASSWORD}@redis_broker:${REDIS_BROKER_PORT}/0
|
||||
|
||||
FLOWER_PORT=8888
|
||||
FLOWER_USER=mouse
|
||||
FLOWER_PASSWORD=changeme
|
||||
|
||||
EMAIL_HOST="smtp.mailgun.org"
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=mail@your.domain.here
|
||||
EMAIL_HOST_PASSWORD=emailpassword123
|
||||
EMAIL_USE_TLS=true
|
||||
EMAIL_USE_SSL=false
|
||||
|
||||
# Set this to true when initializing certbot for domain, false when not
|
||||
CERTBOT_INIT=false
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
**/vendor/**
|
90
.eslintrc.js
Normal file
90
.eslintrc.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
/* global module */
|
||||
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
|
||||
"extends": "eslint:recommended",
|
||||
|
||||
"rules": {
|
||||
// Possible Errors
|
||||
"no-async-promise-executor": "error",
|
||||
"no-await-in-loop": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-confusing-arrow": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-useless-rename": "error",
|
||||
"require-atomic-updates": "error",
|
||||
|
||||
// Best practices
|
||||
"strict": "error",
|
||||
"no-var": "error",
|
||||
|
||||
// Stylistic Issues
|
||||
"arrow-spacing": "error",
|
||||
"capitalized-comments": [
|
||||
"warn",
|
||||
"always",
|
||||
{
|
||||
"ignoreConsecutiveComments": true
|
||||
},
|
||||
],
|
||||
"keyword-spacing": "error",
|
||||
"lines-around-comment": [
|
||||
"error",
|
||||
{
|
||||
"beforeBlockComment": true,
|
||||
"beforeLineComment": true,
|
||||
"allowBlockStart": true,
|
||||
"allowClassStart": true,
|
||||
"allowObjectStart": true,
|
||||
"allowArrayStart": true,
|
||||
},
|
||||
],
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{
|
||||
"max": 1,
|
||||
},
|
||||
],
|
||||
"padded-blocks": [
|
||||
"error",
|
||||
"never",
|
||||
],
|
||||
"padding-line-between-statements": [
|
||||
"error",
|
||||
{
|
||||
// always before return
|
||||
"blankLine": "always",
|
||||
"prev": "*",
|
||||
"next": "return",
|
||||
},
|
||||
{
|
||||
// always before block-like expressions
|
||||
"blankLine": "always",
|
||||
"prev": "*",
|
||||
"next": "block-like",
|
||||
},
|
||||
{
|
||||
// always after variable declaration
|
||||
"blankLine": "always",
|
||||
"prev": [ "const", "let", "var" ],
|
||||
"next": "*",
|
||||
},
|
||||
{
|
||||
// not necessary between variable declaration
|
||||
"blankLine": "any",
|
||||
"prev": [ "const", "let", "var" ],
|
||||
"next": [ "const", "let", "var" ],
|
||||
},
|
||||
],
|
||||
"space-before-blocks": "error",
|
||||
}
|
||||
};
|
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -24,15 +24,15 @@ A clear and concise description of what you expected to happen.
|
|||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
|
13
.github/workflows/black.yml
vendored
Normal file
13
.github/workflows/black.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
name: Lint Python
|
||||
|
||||
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"
|
2
.github/workflows/django-tests.yml
vendored
2
.github/workflows/django-tests.yml
vendored
|
@ -65,4 +65,4 @@ jobs:
|
|||
EMAIL_HOST_PASSWORD: ""
|
||||
EMAIL_USE_TLS: true
|
||||
run: |
|
||||
python manage.py test -v 3
|
||||
python manage.py test
|
||||
|
|
38
.github/workflows/lint-frontend.yaml
vendored
Normal file
38
.github/workflows/lint-frontend.yaml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
name: Lint Frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, ci, frontend ]
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'static/**'
|
||||
- '.eslintrc'
|
||||
- '.stylelintrc'
|
||||
pull_request:
|
||||
branches: [ main, ci, frontend ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint with stylelint and ESLint.
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install modules
|
||||
run: yarn
|
||||
|
||||
# See .stylelintignore for files that are not linted.
|
||||
- name: Run stylelint
|
||||
run: >
|
||||
yarn stylelint bookwyrm/static/**/*.css \
|
||||
--report-needless-disables \
|
||||
--report-invalid-scope-disables
|
||||
|
||||
# See .eslintignore for files that are not linted.
|
||||
- name: Run ESLint
|
||||
run: >
|
||||
yarn eslint bookwyrm/static \
|
||||
--ext .js,.jsx,.ts,.tsx
|
21
.github/workflows/lint-global.yaml
vendored
Normal file
21
.github/workflows/lint-global.yaml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
name: Lint project globally
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, ci ]
|
||||
pull_request:
|
||||
branches: [ main, ci ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint with EditorConfig.
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: EditorConfig
|
||||
uses: greut/eclint-action@v0
|
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -2,6 +2,8 @@
|
|||
/venv
|
||||
*.pyc
|
||||
*.swp
|
||||
**/__pycache__
|
||||
.local
|
||||
|
||||
# VSCode
|
||||
/.vscode
|
||||
|
@ -15,4 +17,13 @@
|
|||
/images/
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
.coverage
|
||||
|
||||
#PyCharm
|
||||
.idea
|
||||
|
||||
#Node tools
|
||||
/node_modules/
|
||||
|
||||
#nginx
|
||||
nginx/default.conf
|
||||
|
|
1
.stylelintignore
Normal file
1
.stylelintignore
Normal file
|
@ -0,0 +1 @@
|
|||
**/vendor/**
|
17
.stylelintrc.js
Normal file
17
.stylelintrc.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
/* global module */
|
||||
|
||||
module.exports = {
|
||||
"extends": "stylelint-config-standard",
|
||||
|
||||
"plugins": [
|
||||
"stylelint-order"
|
||||
],
|
||||
|
||||
"rules": {
|
||||
"order/order": [
|
||||
"custom-properties",
|
||||
"declarations"
|
||||
],
|
||||
"indentation": 4
|
||||
}
|
||||
};
|
|
@ -23,13 +23,13 @@ include:
|
|||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
|
|
182
README.md
182
README.md
|
@ -6,35 +6,20 @@ Social reading and reviewing, decentralized with ActivityPub
|
|||
- [Joining BookWyrm](#joining-bookwyrm)
|
||||
- [Contributing](#contributing)
|
||||
- [About BookWyrm](#about-bookwyrm)
|
||||
- [What it is and isn't](#what-it-is-and-isnt)
|
||||
- [The role of federation](#the-role-of-federation)
|
||||
- [Features](#features)
|
||||
- [Setting up the developer environment](#setting-up-the-developer-environment)
|
||||
- [Installing in Production](#installing-in-production)
|
||||
- [Book data](#book-data)
|
||||
- [What it is and isn't](#what-it-is-and-isnt)
|
||||
- [The role of federation](#the-role-of-federation)
|
||||
- [Features](#features)
|
||||
- [Book data](#book-data)
|
||||
- [Set up Bookwyrm](#set-up-bookwyrm)
|
||||
|
||||
## Joining BookWyrm
|
||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://github.com/mouse-reeve/bookwyrm/blob/main/instances.md) list.
|
||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://docs.joinbookwyrm.com/instances.html) list.
|
||||
|
||||
You can request an invite to https://bookwyrm.social by [email](mailto:mousereeve@riseup.net), [Mastodon direct message](https://friend.camp/@tripofmice), or [Twitter direct message](https://twitter.com/tripofmice).
|
||||
You can request an invite by entering your email address at https://bookwyrm.social.
|
||||
|
||||
|
||||
## Contributing
|
||||
There are many ways you can contribute to this project, regardless of your level of technical expertise.
|
||||
|
||||
### Feedback and feature requests
|
||||
Please feel encouraged and welcome to point out bugs, suggestions, feature requests, and ideas for how things ought to work using [GitHub issues](https://github.com/mouse-reeve/bookwyrm/issues).
|
||||
|
||||
### Code contributions
|
||||
Code contributions are gladly welcomed! If you're not sure where to start, take a look at the ["Good first issue"](https://github.com/mouse-reeve/bookwyrm/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag. Because BookWyrm is a small project, there isn't a lot of formal structure, but there is a huge capacity for one-on-one support, which can look like asking questions as you go, pair programming, video chats, et cetera, so please feel free to reach out.
|
||||
|
||||
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
|
||||
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
|
||||
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).
|
||||
See [contributing](https://docs.joinbookwyrm.com/how-to-contribute.html) for code, translation or monetary contributions.
|
||||
|
||||
## About BookWyrm
|
||||
### What it is and isn't
|
||||
|
@ -46,162 +31,55 @@ BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub,
|
|||
Federation makes it possible to have small, self-determining communities, in contrast to the monolithic service you find on GoodReads or Twitter. An instance can be focused on a particular interest, be just for a group of friends, or anything else that brings people together. Each community can choose which other instances they want to federate with, and moderate and run their community autonomously. Check out https://runyourown.social/ to get a sense of the philosophy and logistics behind small, high-trust social networks.
|
||||
|
||||
### Features
|
||||
Since the project is still in its early stages, the features are growing every day, and there is plenty of room for suggestions and ideas. Open an [issue](https://github.com/mouse-reeve/bookwyrm/issues) to get the conversation going!
|
||||
- Posting about books
|
||||
Since the project is still in its early stages, the features are growing every day, and there is plenty of room for suggestions and ideas. Open an [issue](https://github.com/bookwyrm-social/bookwyrm/issues) to get the conversation going!
|
||||
- Posting about books
|
||||
- Compose reviews, with or without ratings, which are aggregated in the book page
|
||||
- Compose other kinds of statuses about books, such as:
|
||||
- Comments on a book
|
||||
- Quotes or excerpts
|
||||
- Comments on a book
|
||||
- Quotes or excerpts
|
||||
- Reply to statuses
|
||||
- View aggregate reviews of a book across connected BookWyrm instances
|
||||
- Differentiate local and federated reviews and rating in your activity feed
|
||||
- Track reading activity
|
||||
- Track reading activity
|
||||
- Shelve books on default "to-read," "currently reading," and "read" shelves
|
||||
- Create custom shelves
|
||||
- Store started reading/finished reading dates, as well as progress updates along the way
|
||||
- Update followers about reading activity (optionally, and with granular privacy controls)
|
||||
- Create lists of books which can be open to submissions from anyone, curated, or only edited by the creator
|
||||
- Federation with ActivityPub
|
||||
- Federation with ActivityPub
|
||||
- Broadcast and receive user statuses and activity
|
||||
- Share book data between instances to create a networked database of metadata
|
||||
- Identify shared books across instances and aggregate related content
|
||||
- Follow and interact with users across BookWyrm instances
|
||||
- Inter-operate with non-BookWyrm ActivityPub services (currently, Mastodon is supported)
|
||||
- Granular privacy controls
|
||||
- Granular privacy controls
|
||||
- Private, followers-only, and public privacy levels for posting, shelves, and lists
|
||||
- Option for users to manually approve followers
|
||||
- Allow blocking and flagging for moderation
|
||||
|
||||
### The Tech Stack
|
||||
Web backend
|
||||
- [Django](https://www.djangoproject.com/) web server
|
||||
- [PostgreSQL](https://www.postgresql.org/) database
|
||||
- [ActivityPub](http://activitypub.rocks/) federation
|
||||
- [Celery](http://celeryproject.org/) task queuing
|
||||
- [Redis](https://redis.io/) task backend
|
||||
- [Django](https://www.djangoproject.com/) web server
|
||||
- [PostgreSQL](https://www.postgresql.org/) database
|
||||
- [ActivityPub](https://activitypub.rocks/) federation
|
||||
- [Celery](https://docs.celeryproject.org/) task queuing
|
||||
- [Redis](https://redis.io/) task backend
|
||||
- [Redis (again)](https://redis.io/) activity stream manager
|
||||
|
||||
Front end
|
||||
- Django templates
|
||||
- [Bulma.io](https://bulma.io/) css framework
|
||||
- Vanilla JavaScript, in moderation
|
||||
- Django templates
|
||||
- [Bulma.io](https://bulma.io/) css framework
|
||||
- Vanilla JavaScript, in moderation
|
||||
|
||||
Deployment
|
||||
- [Docker](https://www.docker.com/) and docker-compose
|
||||
- [Gunicorn](https://gunicorn.org/) web runner
|
||||
- [Flower](https://github.com/mher/flower) celery monitoring
|
||||
- [Nginx](https://nginx.org/en/) HTTP server
|
||||
|
||||
## Setting up the developer environment
|
||||
|
||||
Set up the environment file:
|
||||
|
||||
``` bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
For most testing, you'll want to use ngrok. Remember to set the DOMAIN in `.env` to your ngrok domain.
|
||||
|
||||
You'll have to install the Docker and docker-compose. When you're ready, run:
|
||||
|
||||
```bash
|
||||
docker-compose build
|
||||
docker-compose run --rm web python manage.py migrate
|
||||
docker-compose run --rm web python manage.py initdb
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Once the build is complete, you can access the instance at `localhost:1333`
|
||||
|
||||
### Editing static files
|
||||
If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` command in order for your changes to have effect. You can do this by running:
|
||||
``` bash
|
||||
./bw-dev collectstatic
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
#### Adding a locale
|
||||
To start translation into a language which is currently supported, run the django-admin `makemessages` command with the language code for the language you want to add (like `de` for German, or `en-gb` for British English):
|
||||
``` bash
|
||||
./bw-dev makemessages -l <language code>
|
||||
```
|
||||
|
||||
#### 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).
|
||||
|
||||
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
|
||||
./bw-dev compilemessages
|
||||
```
|
||||
|
||||
You can add the `-l <language code>` to only compile one language. When you refresh the application, you should see your translations at work.
|
||||
|
||||
## Installing in Production
|
||||
|
||||
This project is still young and isn't, at the moment, very stable, so please proceed with caution when running in production.
|
||||
|
||||
### Server setup
|
||||
- Get a domain name and set up DNS for your server
|
||||
- Set your server up with appropriate firewalls for running a web application (this instruction set is tested against Ubuntu 20.04)
|
||||
- Set up an email service (such as mailgun) and the appropriate SMTP/DNS settings
|
||||
- Install Docker and docker-compose
|
||||
|
||||
### Install and configure BookWyrm
|
||||
|
||||
The `production` branch of BookWyrm contains a number of tools not on the `main` branch that are suited for running in production, such as `docker-compose` changes to update the default commands or configuration of containers, and individual changes to container config to enable things like SSL or regular backups.
|
||||
|
||||
Instructions for running BookWyrm in production:
|
||||
|
||||
- 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
|
||||
- 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
|
||||
- When docker has built successfully, stop the process with `CTRL-C`
|
||||
- Comment out the `command: certonly...` line in `docker-compose.yml`
|
||||
- Run docker-compose in the background with: `docker-compose up -d`
|
||||
- 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
|
||||
|
||||
Congrats! You did it, go to your domain and enjoy the fruits of your labors.
|
||||
|
||||
### Configure your instance
|
||||
- Register a user account in the application UI
|
||||
- Make your account a superuser (warning: do *not* use django's `createsuperuser` command)
|
||||
- On your server, open the django shell
|
||||
`./bw-dev shell`
|
||||
- Load your user and make it a superuser
|
||||
```python
|
||||
from bookwyrm import models
|
||||
user = models.User.objects.get(id=1)
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
```
|
||||
- Go to the site settings (`/settings/site-settings` on your domain) and configure your instance name, description, code of conduct, and toggle whether registration is open on your instance
|
||||
- [Docker](https://www.docker.com/) and docker-compose
|
||||
- [Gunicorn](https://gunicorn.org/) web runner
|
||||
- [Flower](https://github.com/mher/flower) celery monitoring
|
||||
- [Nginx](https://nginx.org/en/) HTTP server
|
||||
|
||||
|
||||
## Book data
|
||||
The application is set up to share book and author data between instances, and get book data from arbitrary outside sources. Right now, the only connector is to OpenLibrary, but other connectors could be written.
|
||||
|
||||
There are three concepts in the book data model:
|
||||
- `Book`, an abstract, high-level concept that could mean either a `Work` or an `Edition`. No data is saved as a `Book`, it serves as shared model for `Work` and `Edition`
|
||||
- `Work`, the theoretical umbrella concept of a book that encompasses every edition of the book, and
|
||||
- `Edition`, a concrete, actually published version of a book
|
||||
|
||||
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.
|
||||
|
||||
## Set up Bookwyrm
|
||||
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up Bookwyrm in a [developer environment](https://docs.joinbookwyrm.com/developer-environment.html) or [production](https://docs.joinbookwyrm.com/installing-in-production.html).
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
''' bring activitypub functions into the namespace '''
|
||||
""" bring activitypub functions into the namespace """
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from .base_activity import ActivityEncoder, Signature, naive_parse
|
||||
from .base_activity import Link, Mention
|
||||
from .base_activity import ActivitySerializerError, resolve_remote_id
|
||||
from .image import Image
|
||||
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
|
||||
from .image import Document, Image
|
||||
from .note import Note, GeneratedNote, Article, Comment, Quotation
|
||||
from .note import Review, Rating
|
||||
from .note import Tombstone
|
||||
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
||||
from .ordered_collection import CollectionItem, ListItem, ShelfItem
|
||||
from .ordered_collection import BookList, Shelf
|
||||
from .person import Person, PublicKey
|
||||
from .response import ActivitypubResponse
|
||||
|
@ -21,9 +23,9 @@ from .verbs import Announce, Like
|
|||
# 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
|
||||
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
||||
activity_objects = {c[0]: c[1] for c in cls_members \
|
||||
if hasattr(c[1], 'to_model')}
|
||||
activity_objects = {c[0]: c[1] for c in cls_members if hasattr(c[1], "to_model")}
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
''' basics for an activitypub serializer '''
|
||||
""" basics for an activitypub serializer """
|
||||
from dataclasses import dataclass, fields, MISSING
|
||||
from json import JSONEncoder
|
||||
|
||||
|
@ -8,48 +8,58 @@ from django.db import IntegrityError, transaction
|
|||
from bookwyrm.connectors import ConnectorException, get_data
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
|
||||
class ActivitySerializerError(ValueError):
|
||||
''' routine problems serializing activitypub json '''
|
||||
""" routine problems serializing activitypub json """
|
||||
|
||||
|
||||
class ActivityEncoder(JSONEncoder):
|
||||
''' used to convert an Activity object into json '''
|
||||
""" used to convert an Activity object into json """
|
||||
|
||||
def default(self, o):
|
||||
return o.__dict__
|
||||
|
||||
|
||||
@dataclass
|
||||
class Link:
|
||||
''' for tagging a book in a status '''
|
||||
""" for tagging a book in a status """
|
||||
|
||||
href: str
|
||||
name: str
|
||||
type: str = 'Link'
|
||||
type: str = "Link"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mention(Link):
|
||||
''' a subtype of Link for mentioning an actor '''
|
||||
type: str = 'Mention'
|
||||
""" a subtype of Link for mentioning an actor """
|
||||
|
||||
type: str = "Mention"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Signature:
|
||||
''' public key block '''
|
||||
""" public key block """
|
||||
|
||||
creator: str
|
||||
created: str
|
||||
signatureValue: str
|
||||
type: str = 'RsaSignature2017'
|
||||
type: str = "RsaSignature2017"
|
||||
|
||||
|
||||
def naive_parse(activity_objects, activity_json, serializer=None):
|
||||
''' this navigates circular import issues '''
|
||||
""" this navigates circular import issues """
|
||||
if not serializer:
|
||||
if activity_json.get('publicKeyPem'):
|
||||
if activity_json.get("publicKeyPem"):
|
||||
# ugh
|
||||
activity_json['type'] = 'PublicKey'
|
||||
activity_json["type"] = "PublicKey"
|
||||
|
||||
activity_type = activity_json.get("type")
|
||||
try:
|
||||
activity_type = activity_json['type']
|
||||
serializer = activity_objects[activity_type]
|
||||
except KeyError as e:
|
||||
# we know this exists and that we can't handle it
|
||||
if activity_type in ["Question"]:
|
||||
return None
|
||||
raise ActivitySerializerError(e)
|
||||
|
||||
return serializer(activity_objects=activity_objects, **activity_json)
|
||||
|
@ -57,25 +67,26 @@ def naive_parse(activity_objects, activity_json, serializer=None):
|
|||
|
||||
@dataclass(init=False)
|
||||
class ActivityObject:
|
||||
''' actor activitypub json '''
|
||||
""" actor activitypub json """
|
||||
|
||||
id: str
|
||||
type: str
|
||||
|
||||
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
|
||||
has a default value '''
|
||||
has a default value"""
|
||||
for field in fields(self):
|
||||
try:
|
||||
value = kwargs[field.name]
|
||||
if value in (None, MISSING):
|
||||
if value in (None, MISSING, {}):
|
||||
raise KeyError()
|
||||
try:
|
||||
is_subclass = issubclass(field.type, ActivityObject)
|
||||
except TypeError:
|
||||
is_subclass = False
|
||||
# serialize a model obj
|
||||
if hasattr(value, 'to_activity'):
|
||||
if hasattr(value, "to_activity"):
|
||||
value = value.to_activity()
|
||||
# parse a dict into the appropriate activity
|
||||
elif is_subclass and isinstance(value, dict):
|
||||
|
@ -83,25 +94,27 @@ class ActivityObject:
|
|||
value = naive_parse(activity_objects, value)
|
||||
else:
|
||||
value = naive_parse(
|
||||
activity_objects, value, serializer=field.type)
|
||||
activity_objects, value, serializer=field.type
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
if field.default == MISSING and \
|
||||
field.default_factory == MISSING:
|
||||
raise ActivitySerializerError(\
|
||||
'Missing required field: %s' % field.name)
|
||||
if field.default == MISSING and field.default_factory == MISSING:
|
||||
raise ActivitySerializerError(
|
||||
"Missing required field: %s" % field.name
|
||||
)
|
||||
value = field.default
|
||||
setattr(self, field.name, value)
|
||||
|
||||
|
||||
def to_model(self, model=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)
|
||||
|
||||
# only reject statuses if we're potentially creating them
|
||||
if allow_create and \
|
||||
hasattr(model, 'ignore_activity') and \
|
||||
model.ignore_activity(self):
|
||||
if (
|
||||
allow_create
|
||||
and hasattr(model, "ignore_activity")
|
||||
and model.ignore_activity(self)
|
||||
):
|
||||
return None
|
||||
|
||||
# check for an existing instance
|
||||
|
@ -142,8 +155,10 @@ class ActivityObject:
|
|||
field.set_field_from_activity(instance, self)
|
||||
|
||||
# reversed relationships in the models
|
||||
for (model_field_name, activity_field_name) in \
|
||||
instance.deserialize_reverse_fields:
|
||||
for (
|
||||
model_field_name,
|
||||
activity_field_name,
|
||||
) in instance.deserialize_reverse_fields:
|
||||
# attachments on Status, for example
|
||||
values = getattr(self, activity_field_name)
|
||||
if values is None or values is MISSING:
|
||||
|
@ -161,13 +176,12 @@ class ActivityObject:
|
|||
instance.__class__.__name__,
|
||||
related_field_name,
|
||||
instance.remote_id,
|
||||
item
|
||||
item,
|
||||
)
|
||||
return instance
|
||||
|
||||
|
||||
def serialize(self):
|
||||
''' convert to dictionary with context attr '''
|
||||
""" convert to dictionary with context attr """
|
||||
data = self.__dict__.copy()
|
||||
# recursively serialize
|
||||
for (k, v) in data.items():
|
||||
|
@ -176,22 +190,19 @@ class ActivityObject:
|
|||
data[k] = v.serialize()
|
||||
except TypeError:
|
||||
pass
|
||||
data = {k:v for (k, v) in data.items() if v is not None}
|
||||
data['@context'] = 'https://www.w3.org/ns/activitystreams'
|
||||
data = {k: v for (k, v) in data.items() if v is not None}
|
||||
data["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
return data
|
||||
|
||||
|
||||
@app.task
|
||||
@transaction.atomic
|
||||
def set_related_field(
|
||||
model_name, origin_model_name, related_field_name,
|
||||
related_remote_id, data):
|
||||
''' load reverse related fields (editions, attachments) without blocking '''
|
||||
model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True)
|
||||
origin_model = apps.get_model(
|
||||
'bookwyrm.%s' % origin_model_name,
|
||||
require_ready=True
|
||||
)
|
||||
model_name, origin_model_name, related_field_name, related_remote_id, data
|
||||
):
|
||||
""" load reverse related fields (editions, attachments) without blocking """
|
||||
model = apps.get_model("bookwyrm.%s" % model_name, require_ready=True)
|
||||
origin_model = apps.get_model("bookwyrm.%s" % origin_model_name, require_ready=True)
|
||||
|
||||
with transaction.atomic():
|
||||
if isinstance(data, str):
|
||||
|
@ -205,63 +216,71 @@ def set_related_field(
|
|||
# this must exist because it's the object that triggered this function
|
||||
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
||||
if not instance:
|
||||
raise ValueError(
|
||||
'Invalid related remote id: %s' % related_remote_id)
|
||||
raise ValueError("Invalid related remote id: %s" % related_remote_id)
|
||||
|
||||
# set the origin's remote id on the activity so it will be there when
|
||||
# the model instance is created
|
||||
# edition.parentWork = instance, for example
|
||||
model_field = getattr(model, related_field_name)
|
||||
if hasattr(model_field, 'activitypub_field'):
|
||||
if hasattr(model_field, "activitypub_field"):
|
||||
setattr(
|
||||
activity,
|
||||
getattr(model_field, 'activitypub_field'),
|
||||
instance.remote_id
|
||||
activity, getattr(model_field, "activitypub_field"), instance.remote_id
|
||||
)
|
||||
item = activity.to_model()
|
||||
|
||||
# if the related field isn't serialized (attachments on Status), then
|
||||
# we have to set it post-creation
|
||||
if not hasattr(model_field, 'activitypub_field'):
|
||||
if not hasattr(model_field, "activitypub_field"):
|
||||
setattr(item, related_field_name, instance)
|
||||
item.save()
|
||||
|
||||
|
||||
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()
|
||||
model = [m for m in models if hasattr(m, 'activity_serializer') and \
|
||||
hasattr(m.activity_serializer, 'type') and \
|
||||
m.activity_serializer.type == activity_type]
|
||||
model = [
|
||||
m
|
||||
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:
|
||||
raise ActivitySerializerError(
|
||||
'No model found for activity type "%s"' % activity_type)
|
||||
'No model found for activity type "%s"' % activity_type
|
||||
)
|
||||
return model[0]
|
||||
|
||||
|
||||
def resolve_remote_id(remote_id, model=None, refresh=False, save=True):
|
||||
''' 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
|
||||
def resolve_remote_id(
|
||||
remote_id, model=None, refresh=False, save=True, get_activity=False
|
||||
):
|
||||
""" 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
|
||||
result = model.find_existing_by_remote_id(remote_id)
|
||||
if result and not refresh:
|
||||
return result
|
||||
return result if not get_activity else result.to_activity_dataclass()
|
||||
|
||||
# load the data and create the object
|
||||
try:
|
||||
data = get_data(remote_id)
|
||||
except (ConnectorException, ConnectionError):
|
||||
except ConnectorException:
|
||||
raise ActivitySerializerError(
|
||||
'Could not connect to host for remote_id in %s model: %s' % \
|
||||
(model.__name__, remote_id))
|
||||
"Could not connect to host for remote_id in: %s" % (remote_id)
|
||||
)
|
||||
# determine the model implicitly, if not provided
|
||||
if not model:
|
||||
model = get_model_from_type(data.get('type'))
|
||||
# or if it's a model with subclasses like Status, check again
|
||||
if not model or hasattr(model.objects, "select_subclasses"):
|
||||
model = get_model_from_type(data.get("type"))
|
||||
|
||||
# check for existing items with shared unique identifiers
|
||||
result = model.find_existing(data)
|
||||
if result and not refresh:
|
||||
return result
|
||||
return result if not get_activity else result.to_activity_dataclass()
|
||||
|
||||
item = model.activity_serializer(**data)
|
||||
if get_activity:
|
||||
return item
|
||||
|
||||
# if we're refreshing, "result" will be set and we'll update it
|
||||
return item.to_model(model=model, instance=result, save=save)
|
||||
|
|
|
@ -1,70 +1,77 @@
|
|||
''' book and author data '''
|
||||
""" book and author data """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from .base_activity import ActivityObject
|
||||
from .image import Image
|
||||
from .image import Document
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Book(ActivityObject):
|
||||
''' serializes an edition or work, abstract '''
|
||||
""" serializes an edition or work, abstract """
|
||||
|
||||
title: str
|
||||
sortTitle: str = ''
|
||||
subtitle: str = ''
|
||||
description: str = ''
|
||||
lastEditedBy: str = None
|
||||
sortTitle: str = ""
|
||||
subtitle: str = ""
|
||||
description: str = ""
|
||||
languages: List[str] = field(default_factory=lambda: [])
|
||||
series: str = ''
|
||||
seriesNumber: str = ''
|
||||
series: str = ""
|
||||
seriesNumber: str = ""
|
||||
subjects: List[str] = field(default_factory=lambda: [])
|
||||
subjectPlaces: List[str] = field(default_factory=lambda: [])
|
||||
|
||||
authors: List[str] = field(default_factory=lambda: [])
|
||||
firstPublishedDate: str = ''
|
||||
publishedDate: str = ''
|
||||
firstPublishedDate: str = ""
|
||||
publishedDate: str = ""
|
||||
|
||||
openlibraryKey: str = ''
|
||||
librarythingKey: str = ''
|
||||
goodreadsKey: str = ''
|
||||
openlibraryKey: str = ""
|
||||
librarythingKey: str = ""
|
||||
goodreadsKey: str = ""
|
||||
|
||||
cover: Image = field(default_factory=lambda: {})
|
||||
type: str = 'Book'
|
||||
cover: Document = None
|
||||
type: str = "Book"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Edition(Book):
|
||||
''' Edition instance of a book object '''
|
||||
""" Edition instance of a book object """
|
||||
|
||||
work: str
|
||||
isbn10: str = ''
|
||||
isbn13: str = ''
|
||||
oclcNumber: str = ''
|
||||
asin: str = ''
|
||||
isbn10: str = ""
|
||||
isbn13: str = ""
|
||||
oclcNumber: str = ""
|
||||
asin: str = ""
|
||||
pages: int = None
|
||||
physicalFormat: str = ''
|
||||
physicalFormat: str = ""
|
||||
publishers: List[str] = field(default_factory=lambda: [])
|
||||
editionRank: int = 0
|
||||
|
||||
type: str = 'Edition'
|
||||
type: str = "Edition"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Work(Book):
|
||||
''' work instance of a book object '''
|
||||
lccn: str = ''
|
||||
defaultEdition: str = ''
|
||||
""" work instance of a book object """
|
||||
|
||||
lccn: str = ""
|
||||
defaultEdition: str = ""
|
||||
editions: List[str] = field(default_factory=lambda: [])
|
||||
type: str = 'Work'
|
||||
type: str = "Work"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Author(ActivityObject):
|
||||
''' author of a book '''
|
||||
""" author of a book """
|
||||
|
||||
name: str
|
||||
lastEditedBy: str = None
|
||||
born: str = None
|
||||
died: str = None
|
||||
aliases: List[str] = field(default_factory=lambda: [])
|
||||
bio: str = ''
|
||||
openlibraryKey: str = ''
|
||||
librarythingKey: str = ''
|
||||
goodreadsKey: str = ''
|
||||
wikipediaLink: str = ''
|
||||
type: str = 'Author'
|
||||
bio: str = ""
|
||||
openlibraryKey: str = ""
|
||||
librarythingKey: str = ""
|
||||
goodreadsKey: str = ""
|
||||
wikipediaLink: str = ""
|
||||
type: str = "Author"
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
''' an image, nothing fancy '''
|
||||
""" an image, nothing fancy """
|
||||
from dataclasses import dataclass
|
||||
from .base_activity import ActivityObject
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Image(ActivityObject):
|
||||
''' image block '''
|
||||
class Document(ActivityObject):
|
||||
""" a document """
|
||||
|
||||
url: str
|
||||
name: str = ''
|
||||
type: str = 'Image'
|
||||
id: str = ''
|
||||
name: str = ""
|
||||
type: str = "Document"
|
||||
id: str = None
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Image(Document):
|
||||
""" an image """
|
||||
|
||||
type: str = "Image"
|
||||
|
|
|
@ -1,69 +1,86 @@
|
|||
''' note serializer and children thereof '''
|
||||
""" note serializer and children thereof """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
from django.apps import apps
|
||||
|
||||
from .base_activity import ActivityObject, Link
|
||||
from .image import Image
|
||||
from .image import Document
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Tombstone(ActivityObject):
|
||||
''' the placeholder for a deleted status '''
|
||||
type: str = 'Tombstone'
|
||||
""" the placeholder for a deleted status """
|
||||
|
||||
def to_model(self, *args, **kwargs):
|
||||
''' this should never really get serialized, just searched for '''
|
||||
model = apps.get_model('bookwyrm.Status')
|
||||
type: str = "Tombstone"
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Note(ActivityObject):
|
||||
''' Note activity '''
|
||||
""" Note activity """
|
||||
|
||||
published: str
|
||||
attributedTo: str
|
||||
content: str = ''
|
||||
content: str = ""
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
replies: Dict = field(default_factory=lambda: {})
|
||||
inReplyTo: str = ''
|
||||
summary: str = ''
|
||||
inReplyTo: str = ""
|
||||
summary: str = ""
|
||||
tag: List[Link] = field(default_factory=lambda: [])
|
||||
attachment: List[Image] = field(default_factory=lambda: [])
|
||||
attachment: List[Document] = field(default_factory=lambda: [])
|
||||
sensitive: bool = False
|
||||
type: str = 'Note'
|
||||
type: str = "Note"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
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
|
||||
type: str = 'Article'
|
||||
type: str = "Article"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class GeneratedNote(Note):
|
||||
''' just a re-typed note '''
|
||||
type: str = 'GeneratedNote'
|
||||
""" just a re-typed note """
|
||||
|
||||
type: str = "GeneratedNote"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Comment(Note):
|
||||
''' like a note but with a book '''
|
||||
""" like a note but with a book """
|
||||
|
||||
inReplyToBook: str
|
||||
type: str = 'Comment'
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Review(Comment):
|
||||
''' a full book review '''
|
||||
name: str = None
|
||||
rating: int = None
|
||||
type: str = 'Review'
|
||||
type: str = "Comment"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Quotation(Comment):
|
||||
''' a quote and commentary on a book '''
|
||||
""" a quote and commentary on a book """
|
||||
|
||||
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 typing import List
|
||||
|
||||
|
@ -7,38 +7,73 @@ from .base_activity import ActivityObject
|
|||
|
||||
@dataclass(init=False)
|
||||
class OrderedCollection(ActivityObject):
|
||||
''' structure of an ordered collection activity '''
|
||||
""" structure of an ordered collection activity """
|
||||
|
||||
totalItems: int
|
||||
first: str
|
||||
last: str = None
|
||||
name: str = None
|
||||
owner: str = None
|
||||
type: str = 'OrderedCollection'
|
||||
type: str = "OrderedCollection"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class OrderedCollectionPrivate(OrderedCollection):
|
||||
''' an ordered collection with privacy settings '''
|
||||
""" an ordered collection with privacy settings """
|
||||
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Shelf(OrderedCollectionPrivate):
|
||||
''' structure of an ordered collection activity '''
|
||||
type: str = 'Shelf'
|
||||
""" structure of an ordered collection activity """
|
||||
|
||||
type: str = "Shelf"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class BookList(OrderedCollectionPrivate):
|
||||
''' structure of an ordered collection activity '''
|
||||
""" structure of an ordered collection activity """
|
||||
|
||||
summary: str = None
|
||||
curation: str = 'closed'
|
||||
type: str = 'BookList'
|
||||
curation: str = "closed"
|
||||
type: str = "BookList"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class OrderedCollectionPage(ActivityObject):
|
||||
''' structure of an ordered collection activity '''
|
||||
""" structure of an ordered collection activity """
|
||||
|
||||
partOf: str
|
||||
orderedItems: List
|
||||
next: str = None
|
||||
prev: str = None
|
||||
type: str = 'OrderedCollectionPage'
|
||||
type: str = "OrderedCollectionPage"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class CollectionItem(ActivityObject):
|
||||
""" an item in a collection """
|
||||
|
||||
actor: str
|
||||
type: str = "CollectionItem"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class ListItem(CollectionItem):
|
||||
""" a book on a list """
|
||||
|
||||
book: str
|
||||
notes: str = None
|
||||
approved: bool = True
|
||||
order: int = None
|
||||
type: str = "ListItem"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class ShelfItem(CollectionItem):
|
||||
""" a book on a list """
|
||||
|
||||
book: str
|
||||
type: str = "ShelfItem"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
''' actor serializer '''
|
||||
""" actor serializer """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict
|
||||
|
||||
|
@ -8,25 +8,28 @@ from .image import Image
|
|||
|
||||
@dataclass(init=False)
|
||||
class PublicKey(ActivityObject):
|
||||
''' public key block '''
|
||||
""" public key block """
|
||||
|
||||
owner: str
|
||||
publicKeyPem: str
|
||||
type: str = 'PublicKey'
|
||||
type: str = "PublicKey"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Person(ActivityObject):
|
||||
''' actor activitypub json '''
|
||||
""" actor activitypub json """
|
||||
|
||||
preferredUsername: str
|
||||
inbox: str
|
||||
outbox: str
|
||||
followers: str
|
||||
publicKey: PublicKey
|
||||
endpoints: Dict
|
||||
followers: str = None
|
||||
following: str = None
|
||||
outbox: str = None
|
||||
endpoints: Dict = None
|
||||
name: str = None
|
||||
summary: str = None
|
||||
icon: Image = field(default_factory=lambda: {})
|
||||
bookwyrmUser: bool = False
|
||||
manuallyApprovesFollowers: str = False
|
||||
discoverable: str = True
|
||||
type: str = 'Person'
|
||||
discoverable: str = False
|
||||
type: str = "Person"
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.http import JsonResponse
|
|||
|
||||
from .base_activity import ActivityEncoder
|
||||
|
||||
|
||||
class ActivitypubResponse(JsonResponse):
|
||||
"""
|
||||
A class to be used in any place that's serializing responses for
|
||||
|
@ -9,10 +10,17 @@ class ActivitypubResponse(JsonResponse):
|
|||
configures some stuff beforehand. Made to be a drop-in replacement of
|
||||
JsonResponse.
|
||||
"""
|
||||
def __init__(self, data, encoder=ActivityEncoder, safe=False,
|
||||
json_dumps_params=None, **kwargs):
|
||||
|
||||
if 'content_type' not in kwargs:
|
||||
kwargs['content_type'] = 'application/activity+json'
|
||||
def __init__(
|
||||
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)
|
||||
|
|
|
@ -1,168 +1,207 @@
|
|||
''' undo wrapper activity '''
|
||||
from dataclasses import dataclass
|
||||
""" activities that do things """
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
from django.apps import apps
|
||||
|
||||
from .base_activity import ActivityObject, Signature, resolve_remote_id
|
||||
from .book import Edition
|
||||
from .ordered_collection import CollectionItem
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Verb(ActivityObject):
|
||||
''' generic fields for activities - maybe an unecessary level of
|
||||
abstraction but w/e '''
|
||||
"""generic fields for activities """
|
||||
|
||||
actor: str
|
||||
object: ActivityObject
|
||||
|
||||
def action(self):
|
||||
''' usually we just want to save, this can be overridden as needed '''
|
||||
self.object.to_model()
|
||||
""" usually we just want to update and save """
|
||||
# self.object may return None if the object is invalid in an expected way
|
||||
# ie, Question type
|
||||
if self.object:
|
||||
self.object.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Create(Verb):
|
||||
''' Create activity '''
|
||||
to: List
|
||||
cc: List
|
||||
""" Create activity """
|
||||
|
||||
to: List[str]
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
signature: Signature = None
|
||||
type: str = 'Create'
|
||||
type: str = "Create"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Delete(Verb):
|
||||
''' Create activity '''
|
||||
to: List
|
||||
cc: List
|
||||
type: str = 'Delete'
|
||||
""" Create activity """
|
||||
|
||||
to: List[str]
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
type: str = "Delete"
|
||||
|
||||
def action(self):
|
||||
''' find and delete the activity object '''
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
obj.delete()
|
||||
""" find and delete the activity object """
|
||||
if not self.object:
|
||||
return
|
||||
|
||||
if isinstance(self.object, str):
|
||||
# Deleted users are passed as strings. Not wild about this fix
|
||||
model = apps.get_model("bookwyrm.User")
|
||||
obj = model.find_existing_by_remote_id(self.object)
|
||||
else:
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
|
||||
if obj:
|
||||
obj.delete()
|
||||
# if we can't find it, we don't need to delete it because we don't have it
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Update(Verb):
|
||||
''' Update activity '''
|
||||
to: List
|
||||
type: str = 'Update'
|
||||
""" Update activity """
|
||||
|
||||
to: List[str]
|
||||
type: str = "Update"
|
||||
|
||||
def action(self):
|
||||
''' update a model instance from the dataclass '''
|
||||
self.object.to_model(allow_create=False)
|
||||
""" update a model instance from the dataclass """
|
||||
if self.object:
|
||||
self.object.to_model(allow_create=False)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Undo(Verb):
|
||||
''' Undo an activity '''
|
||||
type: str = 'Undo'
|
||||
""" Undo an activity """
|
||||
|
||||
type: str = "Undo"
|
||||
|
||||
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....
|
||||
# (because you Reject a request and Undo a follow
|
||||
model = None
|
||||
if self.object.type == 'Follow':
|
||||
model = apps.get_model('bookwyrm.UserFollows')
|
||||
obj = self.object.to_model(model=model, save=False, allow_create=False)
|
||||
if self.object.type == "Follow":
|
||||
model = apps.get_model("bookwyrm.UserFollows")
|
||||
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()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Follow(Verb):
|
||||
''' Follow activity '''
|
||||
""" Follow activity """
|
||||
|
||||
object: str
|
||||
type: str = 'Follow'
|
||||
type: str = "Follow"
|
||||
|
||||
def action(self):
|
||||
''' relationship save '''
|
||||
""" relationship save """
|
||||
self.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Block(Verb):
|
||||
''' Block activity '''
|
||||
""" Block activity """
|
||||
|
||||
object: str
|
||||
type: str = 'Block'
|
||||
type: str = "Block"
|
||||
|
||||
def action(self):
|
||||
''' relationship save '''
|
||||
""" relationship save """
|
||||
self.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Accept(Verb):
|
||||
''' Accept activity '''
|
||||
""" Accept activity """
|
||||
|
||||
object: Follow
|
||||
type: str = 'Accept'
|
||||
type: str = "Accept"
|
||||
|
||||
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.accept()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Reject(Verb):
|
||||
''' Reject activity '''
|
||||
""" Reject activity """
|
||||
|
||||
object: Follow
|
||||
type: str = 'Reject'
|
||||
type: str = "Reject"
|
||||
|
||||
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.reject()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Add(Verb):
|
||||
'''Add activity '''
|
||||
target: str
|
||||
object: Edition
|
||||
type: str = 'Add'
|
||||
notes: str = None
|
||||
order: int = 0
|
||||
approved: bool = True
|
||||
"""Add activity """
|
||||
|
||||
target: ActivityObject
|
||||
object: CollectionItem
|
||||
type: str = "Add"
|
||||
|
||||
def action(self):
|
||||
''' add obj to collection '''
|
||||
target = resolve_remote_id(self.target, refresh=False)
|
||||
# we want to related field that isn't the book, this is janky af sorry
|
||||
model = [t for t in type(target)._meta.related_objects \
|
||||
if t.name != 'edition'][0].related_model
|
||||
self.to_model(model=model)
|
||||
""" figure out the target to assign the item to a collection """
|
||||
target = resolve_remote_id(self.target)
|
||||
item = self.object.to_model(save=False)
|
||||
setattr(item, item.collection_field, target)
|
||||
item.save()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Remove(Verb):
|
||||
'''Remove activity '''
|
||||
target: ActivityObject
|
||||
type: str = 'Remove'
|
||||
class Remove(Add):
|
||||
"""Remove activity """
|
||||
|
||||
type: str = "Remove"
|
||||
|
||||
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.delete()
|
||||
if obj:
|
||||
obj.delete()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Like(Verb):
|
||||
''' a user faving an object '''
|
||||
""" a user faving an object """
|
||||
|
||||
object: str
|
||||
type: str = 'Like'
|
||||
type: str = "Like"
|
||||
|
||||
def action(self):
|
||||
''' like '''
|
||||
""" like """
|
||||
self.to_model()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Announce(Verb):
|
||||
''' boosting a status '''
|
||||
""" boosting a status """
|
||||
|
||||
published: str
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
object: str
|
||||
type: str = 'Announce'
|
||||
type: str = "Announce"
|
||||
|
||||
def action(self):
|
||||
''' boost '''
|
||||
""" boost """
|
||||
self.to_model()
|
||||
|
|
269
bookwyrm/activitystreams.py
Normal file
269
bookwyrm/activitystreams.py
Normal file
|
@ -0,0 +1,269 @@
|
|||
""" access the activity streams stored in redis """
|
||||
from django.dispatch import receiver
|
||||
from django.db.models import signals, Q
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.redis_store import RedisStore, r
|
||||
from bookwyrm.views.helpers import privacy_filter
|
||||
|
||||
|
||||
class ActivityStream(RedisStore):
|
||||
""" a category of activity stream (like home, local, federated) """
|
||||
|
||||
def stream_id(self, user):
|
||||
""" the redis key for this user's instance of this stream """
|
||||
return "{}-{}".format(user.id, self.key)
|
||||
|
||||
def unread_id(self, user):
|
||||
""" the redis key for this user's unread count for this stream """
|
||||
return "{}-unread".format(self.stream_id(user))
|
||||
|
||||
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||
""" statuses are sorted by date published """
|
||||
return obj.published_date.timestamp()
|
||||
|
||||
def add_status(self, status):
|
||||
""" add a status to users' feeds """
|
||||
# the pipeline contains all the add-to-stream activities
|
||||
pipeline = self.add_object_to_related_stores(status, execute=False)
|
||||
|
||||
for user in self.get_audience(status):
|
||||
# add to the unread status count
|
||||
pipeline.incr(self.unread_id(user))
|
||||
|
||||
# and go!
|
||||
pipeline.execute()
|
||||
|
||||
def add_user_statuses(self, viewer, user):
|
||||
""" add a user's statuses to another user's feed """
|
||||
# only add the statuses that the viewer should be able to see (ie, not dms)
|
||||
statuses = privacy_filter(viewer, user.status_set.all())
|
||||
self.bulk_add_objects_to_store(statuses, self.stream_id(viewer))
|
||||
|
||||
def remove_user_statuses(self, viewer, user):
|
||||
""" remove a user's status from another user's feed """
|
||||
# remove all so that followers only statuses are removed
|
||||
statuses = user.status_set.all()
|
||||
self.bulk_remove_objects_from_store(statuses, self.stream_id(viewer))
|
||||
|
||||
def get_activity_stream(self, user):
|
||||
""" load the statuses to be displayed """
|
||||
# clear unreads for this feed
|
||||
r.set(self.unread_id(user), 0)
|
||||
|
||||
statuses = self.get_store(self.stream_id(user))
|
||||
return (
|
||||
models.Status.objects.select_subclasses()
|
||||
.filter(id__in=statuses)
|
||||
.order_by("-published_date")
|
||||
)
|
||||
|
||||
def get_unread_count(self, user):
|
||||
""" get the unread status count for this user's feed """
|
||||
return int(r.get(self.unread_id(user)) or 0)
|
||||
|
||||
def populate_streams(self, user):
|
||||
""" go from zero to a timeline """
|
||||
self.populate_store(self.stream_id(user))
|
||||
|
||||
def get_audience(self, status): # pylint: disable=no-self-use
|
||||
""" given a status, what users should see it """
|
||||
# direct messages don't appeard in feeds, direct comments/reviews/etc do
|
||||
if status.privacy == "direct" and status.status_type == "Note":
|
||||
return []
|
||||
|
||||
# everybody who could plausibly see this status
|
||||
audience = models.User.objects.filter(
|
||||
is_active=True,
|
||||
local=True, # we only create feeds for users of this instance
|
||||
).exclude(
|
||||
Q(id__in=status.user.blocks.all()) | Q(blocks=status.user) # not blocked
|
||||
)
|
||||
|
||||
# only visible to the poster and mentioned users
|
||||
if status.privacy == "direct":
|
||||
audience = audience.filter(
|
||||
Q(id=status.user.id) # if the user is the post's author
|
||||
| Q(id__in=status.mention_users.all()) # if the user is mentioned
|
||||
)
|
||||
# only visible to the poster's followers and tagged users
|
||||
elif status.privacy == "followers":
|
||||
audience = audience.filter(
|
||||
Q(id=status.user.id) # if the user is the post's author
|
||||
| Q(following=status.user) # if the user is following the author
|
||||
)
|
||||
return audience.distinct()
|
||||
|
||||
def get_stores_for_object(self, obj):
|
||||
return [self.stream_id(u) for u in self.get_audience(obj)]
|
||||
|
||||
def get_statuses_for_user(self, user): # pylint: disable=no-self-use
|
||||
""" given a user, what statuses should they see on this stream """
|
||||
return privacy_filter(
|
||||
user,
|
||||
models.Status.objects.select_subclasses(),
|
||||
privacy_levels=["public", "unlisted", "followers"],
|
||||
)
|
||||
|
||||
def get_objects_for_store(self, store):
|
||||
user = models.User.objects.get(id=store.split("-")[0])
|
||||
return self.get_statuses_for_user(user)
|
||||
|
||||
|
||||
class HomeStream(ActivityStream):
|
||||
""" users you follow """
|
||||
|
||||
key = "home"
|
||||
|
||||
def get_audience(self, status):
|
||||
audience = super().get_audience(status)
|
||||
if not audience:
|
||||
return []
|
||||
return audience.filter(
|
||||
Q(id=status.user.id) # if the user is the post's author
|
||||
| Q(following=status.user) # if the user is following the author
|
||||
).distinct()
|
||||
|
||||
def get_statuses_for_user(self, user):
|
||||
return privacy_filter(
|
||||
user,
|
||||
models.Status.objects.select_subclasses(),
|
||||
privacy_levels=["public", "unlisted", "followers"],
|
||||
following_only=True,
|
||||
)
|
||||
|
||||
|
||||
class LocalStream(ActivityStream):
|
||||
""" users you follow """
|
||||
|
||||
key = "local"
|
||||
|
||||
def get_audience(self, status):
|
||||
# this stream wants no part in non-public statuses
|
||||
if status.privacy != "public" or not status.user.local:
|
||||
return []
|
||||
return super().get_audience(status)
|
||||
|
||||
def get_statuses_for_user(self, user):
|
||||
# all public statuses by a local user
|
||||
return privacy_filter(
|
||||
user,
|
||||
models.Status.objects.select_subclasses().filter(user__local=True),
|
||||
privacy_levels=["public"],
|
||||
)
|
||||
|
||||
|
||||
class FederatedStream(ActivityStream):
|
||||
""" users you follow """
|
||||
|
||||
key = "federated"
|
||||
|
||||
def get_audience(self, status):
|
||||
# this stream wants no part in non-public statuses
|
||||
if status.privacy != "public":
|
||||
return []
|
||||
return super().get_audience(status)
|
||||
|
||||
def get_statuses_for_user(self, user):
|
||||
return privacy_filter(
|
||||
user,
|
||||
models.Status.objects.select_subclasses(),
|
||||
privacy_levels=["public"],
|
||||
)
|
||||
|
||||
|
||||
streams = {
|
||||
"home": HomeStream(),
|
||||
"local": LocalStream(),
|
||||
"federated": FederatedStream(),
|
||||
}
|
||||
|
||||
|
||||
@receiver(signals.post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def add_status_on_create(sender, instance, created, *args, **kwargs):
|
||||
""" add newly created statuses to activity feeds """
|
||||
# we're only interested in new statuses
|
||||
if not issubclass(sender, models.Status):
|
||||
return
|
||||
|
||||
if instance.deleted:
|
||||
for stream in streams.values():
|
||||
stream.remove_object_from_related_stores(instance)
|
||||
return
|
||||
|
||||
if not created:
|
||||
return
|
||||
|
||||
# iterates through Home, Local, Federated
|
||||
for stream in streams.values():
|
||||
stream.add_status(instance)
|
||||
|
||||
|
||||
@receiver(signals.post_delete, sender=models.Boost)
|
||||
# pylint: disable=unused-argument
|
||||
def remove_boost_on_delete(sender, instance, *args, **kwargs):
|
||||
""" boosts are deleted """
|
||||
# we're only interested in new statuses
|
||||
for stream in streams.values():
|
||||
stream.remove_object_from_related_stores(instance)
|
||||
|
||||
|
||||
@receiver(signals.post_save, sender=models.UserFollows)
|
||||
# pylint: disable=unused-argument
|
||||
def add_statuses_on_follow(sender, instance, created, *args, **kwargs):
|
||||
""" add a newly followed user's statuses to feeds """
|
||||
if not created or not instance.user_subject.local:
|
||||
return
|
||||
HomeStream().add_user_statuses(instance.user_subject, instance.user_object)
|
||||
|
||||
|
||||
@receiver(signals.post_delete, sender=models.UserFollows)
|
||||
# pylint: disable=unused-argument
|
||||
def remove_statuses_on_unfollow(sender, instance, *args, **kwargs):
|
||||
""" remove statuses from a feed on unfollow """
|
||||
if not instance.user_subject.local:
|
||||
return
|
||||
HomeStream().remove_user_statuses(instance.user_subject, instance.user_object)
|
||||
|
||||
|
||||
@receiver(signals.post_save, sender=models.UserBlocks)
|
||||
# pylint: disable=unused-argument
|
||||
def remove_statuses_on_block(sender, instance, *args, **kwargs):
|
||||
""" remove statuses from all feeds on block """
|
||||
# blocks apply ot all feeds
|
||||
if instance.user_subject.local:
|
||||
for stream in streams.values():
|
||||
stream.remove_user_statuses(instance.user_subject, instance.user_object)
|
||||
|
||||
# and in both directions
|
||||
if instance.user_object.local:
|
||||
for stream in streams.values():
|
||||
stream.remove_user_statuses(instance.user_object, instance.user_subject)
|
||||
|
||||
|
||||
@receiver(signals.post_delete, sender=models.UserBlocks)
|
||||
# pylint: disable=unused-argument
|
||||
def add_statuses_on_unblock(sender, instance, *args, **kwargs):
|
||||
""" remove statuses from all feeds on block """
|
||||
public_streams = [LocalStream(), FederatedStream()]
|
||||
# add statuses back to streams with statuses from anyone
|
||||
if instance.user_subject.local:
|
||||
for stream in public_streams:
|
||||
stream.add_user_statuses(instance.user_subject, instance.user_object)
|
||||
|
||||
# add statuses back to streams with statuses from anyone
|
||||
if instance.user_object.local:
|
||||
for stream in public_streams:
|
||||
stream.add_user_statuses(instance.user_object, instance.user_subject)
|
||||
|
||||
|
||||
@receiver(signals.post_save, sender=models.User)
|
||||
# pylint: disable=unused-argument
|
||||
def populate_streams_on_account_create(sender, instance, created, *args, **kwargs):
|
||||
""" build a user's feeds when they join """
|
||||
if not created or not instance.local:
|
||||
return
|
||||
|
||||
for stream in streams.values():
|
||||
stream.populate_streams(instance)
|
|
@ -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 bookwyrm import models
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
''' bring connectors into the namespace '''
|
||||
""" bring connectors into the namespace """
|
||||
from .settings import CONNECTORS
|
||||
from .abstract_connector import ConnectorException
|
||||
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 dataclasses import asdict, dataclass
|
||||
import logging
|
||||
|
@ -13,8 +13,11 @@ from .connector_manager import load_more_data, ConnectorException
|
|||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AbstractMinimalConnector(ABC):
|
||||
''' just the bare bones, for other bookwyrm instances '''
|
||||
""" just the bare bones, for other bookwyrm instances """
|
||||
|
||||
def __init__(self, identifier):
|
||||
# load connector settings
|
||||
info = models.Connector.objects.get(identifier=identifier)
|
||||
|
@ -22,82 +25,94 @@ class AbstractMinimalConnector(ABC):
|
|||
|
||||
# the things in the connector model to copy over
|
||||
self_fields = [
|
||||
'base_url',
|
||||
'books_url',
|
||||
'covers_url',
|
||||
'search_url',
|
||||
'max_query_count',
|
||||
'name',
|
||||
'identifier',
|
||||
'local'
|
||||
"base_url",
|
||||
"books_url",
|
||||
"covers_url",
|
||||
"search_url",
|
||||
"isbn_search_url",
|
||||
"max_query_count",
|
||||
"name",
|
||||
"identifier",
|
||||
"local",
|
||||
]
|
||||
for field in self_fields:
|
||||
setattr(self, field, getattr(info, field))
|
||||
|
||||
def search(self, query, min_confidence=None):
|
||||
''' free text search '''
|
||||
""" free text search """
|
||||
params = {}
|
||||
if min_confidence:
|
||||
params['min_confidence'] = min_confidence
|
||||
params["min_confidence"] = min_confidence
|
||||
|
||||
resp = requests.get(
|
||||
'%s%s' % (self.search_url, query),
|
||||
data = get_data(
|
||||
"%s%s" % (self.search_url, query),
|
||||
params=params,
|
||||
headers={
|
||||
'Accept': 'application/json; charset=utf-8',
|
||||
'User-Agent': settings.USER_AGENT,
|
||||
},
|
||||
)
|
||||
if not resp.ok:
|
||||
resp.raise_for_status()
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError as e:
|
||||
logger.exception(e)
|
||||
raise ConnectorException('Unable to parse json response', e)
|
||||
results = []
|
||||
|
||||
for doc in self.parse_search_data(data)[:10]:
|
||||
results.append(self.format_search_result(doc))
|
||||
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
|
||||
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
|
||||
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
|
||||
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):
|
||||
''' generic book data connector '''
|
||||
""" generic book data connector """
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
# fields we want to look for in book data to copy over
|
||||
# title we handle separately.
|
||||
self.book_mappings = []
|
||||
|
||||
|
||||
def is_available(self):
|
||||
''' check if you're allowed to use this connector '''
|
||||
""" check if you're allowed to use this connector """
|
||||
if self.max_query_count is not None:
|
||||
if self.connector.query_count >= self.max_query_count:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_or_create_book(self, remote_id):
|
||||
''' translate arbitrary json into an Activitypub dataclass '''
|
||||
""" translate arbitrary json into an Activitypub dataclass """
|
||||
# first, check if we have the origin_id saved
|
||||
existing = models.Edition.find_existing_by_remote_id(remote_id) or \
|
||||
models.Work.find_existing_by_remote_id(remote_id)
|
||||
existing = models.Edition.find_existing_by_remote_id(
|
||||
remote_id
|
||||
) or models.Work.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
if hasattr(existing, 'get_default_editon'):
|
||||
if hasattr(existing, "get_default_editon"):
|
||||
return existing.get_default_editon()
|
||||
return existing
|
||||
|
||||
|
@ -121,7 +136,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
edition_data = 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():
|
||||
# create activitypub object
|
||||
|
@ -135,11 +150,10 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
load_more_data.delay(self.connector.id, work.id)
|
||||
return edition
|
||||
|
||||
|
||||
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['work'] = work.remote_id
|
||||
mapped_data["work"] = work.remote_id
|
||||
edition_activity = activitypub.Edition(**mapped_data)
|
||||
edition = edition_activity.to_model(model=models.Edition)
|
||||
edition.connector = self.connector
|
||||
|
@ -156,9 +170,8 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
|
||||
return edition
|
||||
|
||||
|
||||
def get_or_create_author(self, remote_id):
|
||||
''' load that author '''
|
||||
""" load that author """
|
||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
return existing
|
||||
|
@ -166,52 +179,62 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
data = get_data(remote_id)
|
||||
|
||||
mapped_data = dict_from_mappings(data, self.author_mappings)
|
||||
activity = activitypub.Author(**mapped_data)
|
||||
try:
|
||||
activity = activitypub.Author(**mapped_data)
|
||||
except activitypub.ActivitySerializerError:
|
||||
return None
|
||||
|
||||
# this will dedupe
|
||||
return activity.to_model(model=models.Author)
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def is_work_data(self, data):
|
||||
''' differentiate works and editions '''
|
||||
""" differentiate works and editions """
|
||||
|
||||
@abstractmethod
|
||||
def get_edition_from_work_data(self, data):
|
||||
''' every work needs at least one edition '''
|
||||
""" every work needs at least one edition """
|
||||
|
||||
@abstractmethod
|
||||
def get_work_from_edition_data(self, data):
|
||||
''' every edition needs a work '''
|
||||
""" every edition needs a work """
|
||||
|
||||
@abstractmethod
|
||||
def get_authors_from_data(self, data):
|
||||
''' load author data '''
|
||||
""" load author data """
|
||||
|
||||
@abstractmethod
|
||||
def expand_book_data(self, book):
|
||||
''' get more info on a book '''
|
||||
""" get more info on a book """
|
||||
|
||||
|
||||
def dict_from_mappings(data, mappings):
|
||||
''' create a dict in Activitypub format, using mappings supplies by
|
||||
the subclass '''
|
||||
"""create a dict in Activitypub format, using mappings supplies by
|
||||
the subclass"""
|
||||
result = {}
|
||||
for mapping in mappings:
|
||||
result[mapping.local_field] = mapping.get_value(data)
|
||||
return result
|
||||
|
||||
|
||||
def get_data(url):
|
||||
''' wrapper for request.get '''
|
||||
def get_data(url, params=None):
|
||||
""" wrapper for request.get """
|
||||
# check if the url is blocked
|
||||
if models.FederatedServer.is_blocked(url):
|
||||
raise ConnectorException(
|
||||
"Attempting to load data from blocked url: {:s}".format(url)
|
||||
)
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
url,
|
||||
params=params,
|
||||
headers={
|
||||
'Accept': 'application/json; charset=utf-8',
|
||||
'User-Agent': settings.USER_AGENT,
|
||||
"Accept": "application/json; charset=utf-8",
|
||||
"User-Agent": settings.USER_AGENT,
|
||||
},
|
||||
)
|
||||
except (RequestError, SSLError) as e:
|
||||
except (RequestError, SSLError, ConnectionError) as e:
|
||||
logger.exception(e)
|
||||
raise ConnectorException()
|
||||
|
||||
|
@ -227,12 +250,12 @@ def get_data(url):
|
|||
|
||||
|
||||
def get_image(url):
|
||||
''' wrapper for requesting an image '''
|
||||
""" wrapper for requesting an image """
|
||||
try:
|
||||
resp = requests.get(
|
||||
url,
|
||||
headers={
|
||||
'User-Agent': settings.USER_AGENT,
|
||||
"User-Agent": settings.USER_AGENT,
|
||||
},
|
||||
)
|
||||
except (RequestError, SSLError) as e:
|
||||
|
@ -245,27 +268,31 @@ def get_image(url):
|
|||
|
||||
@dataclass
|
||||
class SearchResult:
|
||||
''' standardized search result object '''
|
||||
""" standardized search result object """
|
||||
|
||||
title: str
|
||||
key: str
|
||||
author: str
|
||||
year: str
|
||||
connector: object
|
||||
author: str = None
|
||||
year: str = None
|
||||
cover: str = None
|
||||
confidence: int = 1
|
||||
|
||||
def __repr__(self):
|
||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||
self.key, self.title, self.author)
|
||||
self.key, self.title, self.author
|
||||
)
|
||||
|
||||
def json(self):
|
||||
''' serialize a connector for json response '''
|
||||
""" serialize a connector for json response """
|
||||
serialized = asdict(self)
|
||||
del serialized['connector']
|
||||
del serialized["connector"]
|
||||
return serialized
|
||||
|
||||
|
||||
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):
|
||||
noop = lambda x: x
|
||||
|
||||
|
@ -274,11 +301,11 @@ class Mapping:
|
|||
self.formatter = formatter or noop
|
||||
|
||||
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)
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return self.formatter(value)
|
||||
except:# pylint: disable=bare-except
|
||||
except: # pylint: disable=bare-except
|
||||
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 .abstract_connector import AbstractMinimalConnector, SearchResult
|
||||
|
||||
|
||||
class Connector(AbstractMinimalConnector):
|
||||
''' this is basically just for search '''
|
||||
""" this is basically just for search """
|
||||
|
||||
def get_or_create_book(self, remote_id):
|
||||
edition = activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
||||
|
@ -17,5 +17,11 @@ class Connector(AbstractMinimalConnector):
|
|||
return data
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
search_result['connector'] = self
|
||||
search_result["connector"] = self
|
||||
return SearchResult(**search_result)
|
||||
|
||||
def parse_isbn_search_data(self, data):
|
||||
return data
|
||||
|
||||
def format_isbn_search_result(self, search_result):
|
||||
return self.format_search_result(search_result)
|
||||
|
|
|
@ -1,48 +1,86 @@
|
|||
''' interface with whatever connectors the app has '''
|
||||
""" interface with whatever connectors the app has """
|
||||
import importlib
|
||||
import logging
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.db.models import signals
|
||||
|
||||
from requests import HTTPError
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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):
|
||||
''' find books based on arbitary keywords '''
|
||||
""" find books based on arbitary keywords """
|
||||
if not query:
|
||||
return []
|
||||
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()
|
||||
for connector in get_connectors():
|
||||
try:
|
||||
result_set = connector.search(query, min_confidence=min_confidence)
|
||||
except (HTTPError, ConnectorException):
|
||||
continue
|
||||
result_set = None
|
||||
if maybe_isbn:
|
||||
# Search on ISBN
|
||||
if not connector.isbn_search_url or connector.isbn_search_url == "":
|
||||
result_set = []
|
||||
else:
|
||||
try:
|
||||
result_set = connector.isbn_search(isbn)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.exception(e)
|
||||
continue
|
||||
|
||||
result_set = [r for r in result_set \
|
||||
if dedup_slug(r) not in result_index]
|
||||
# if no isbn search or results, we fallback to generic search
|
||||
if result_set in (None, []):
|
||||
try:
|
||||
result_set = connector.search(query, min_confidence=min_confidence)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
# we don't want *any* error to crash the whole search page
|
||||
logger.exception(e)
|
||||
continue
|
||||
|
||||
# if the search results look the same, ignore them
|
||||
result_set = [r for r in result_set if dedup_slug(r) not in result_index]
|
||||
# `|=` concats two sets. WE ARE GETTING FANCY HERE
|
||||
result_index |= set(dedup_slug(r) for r in result_set)
|
||||
results.append({
|
||||
'connector': connector,
|
||||
'results': result_set,
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"connector": connector,
|
||||
"results": result_set,
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
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))
|
||||
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):
|
||||
''' search until you find a result that fits '''
|
||||
""" search until you find a result that fits """
|
||||
for connector in get_connectors():
|
||||
result = connector.search(query, min_confidence=min_confidence)
|
||||
if result:
|
||||
|
@ -51,29 +89,29 @@ def first_search_result(query, min_confidence=0.1):
|
|||
|
||||
|
||||
def get_connectors():
|
||||
''' load all connectors '''
|
||||
for info in models.Connector.objects.order_by('priority').all():
|
||||
""" load all connectors """
|
||||
for info in models.Connector.objects.order_by("priority").all():
|
||||
yield load_connector(info)
|
||||
|
||||
|
||||
def get_or_create_connector(remote_id):
|
||||
''' get the connector related to the author's server '''
|
||||
""" get the connector related to the object's server """
|
||||
url = urlparse(remote_id)
|
||||
identifier = url.netloc
|
||||
if not identifier:
|
||||
raise ValueError('Invalid remote id')
|
||||
raise ValueError("Invalid remote id")
|
||||
|
||||
try:
|
||||
connector_info = models.Connector.objects.get(identifier=identifier)
|
||||
except models.Connector.DoesNotExist:
|
||||
connector_info = models.Connector.objects.create(
|
||||
identifier=identifier,
|
||||
connector_file='bookwyrm_connector',
|
||||
base_url='https://%s' % identifier,
|
||||
books_url='https://%s/book' % identifier,
|
||||
covers_url='https://%s/images/covers' % identifier,
|
||||
search_url='https://%s/search?q=' % identifier,
|
||||
priority=2
|
||||
connector_file="bookwyrm_connector",
|
||||
base_url="https://%s" % identifier,
|
||||
books_url="https://%s/book" % identifier,
|
||||
covers_url="https://%s/images/covers" % identifier,
|
||||
search_url="https://%s/search?q=" % identifier,
|
||||
priority=2,
|
||||
)
|
||||
|
||||
return load_connector(connector_info)
|
||||
|
@ -81,7 +119,7 @@ def get_or_create_connector(remote_id):
|
|||
|
||||
@app.task
|
||||
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 = load_connector(connector_info)
|
||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||
|
@ -89,8 +127,16 @@ def load_more_data(connector_id, book_id):
|
|||
|
||||
|
||||
def load_connector(connector_info):
|
||||
''' instantiate the connector class '''
|
||||
""" instantiate the connector class """
|
||||
connector = importlib.import_module(
|
||||
'bookwyrm.connectors.%s' % connector_info.connector_file
|
||||
"bookwyrm.connectors.%s" % connector_info.connector_file
|
||||
)
|
||||
return connector.Connector(connector_info.identifier)
|
||||
|
||||
|
||||
@receiver(signals.post_save, sender="bookwyrm.FederatedServer")
|
||||
# pylint: disable=unused-argument
|
||||
def create_connector(sender, instance, created, *args, **kwargs):
|
||||
""" create a connector to an external bookwyrm server """
|
||||
if instance.application_type == "bookwyrm":
|
||||
get_or_create_connector("https://{:s}".format(instance.server_name))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
''' openlibrary data connector '''
|
||||
""" openlibrary data connector """
|
||||
import re
|
||||
|
||||
from bookwyrm import models
|
||||
|
@ -9,132 +9,142 @@ from .openlibrary_languages import languages
|
|||
|
||||
|
||||
class Connector(AbstractConnector):
|
||||
''' instantiate a connector for OL '''
|
||||
""" instantiate a connector for OL """
|
||||
|
||||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
get_first = lambda a: a[0]
|
||||
get_remote_id = lambda a: self.base_url + a
|
||||
self.book_mappings = [
|
||||
Mapping('title'),
|
||||
Mapping('id', remote_field='key', formatter=get_remote_id),
|
||||
Mapping("title"),
|
||||
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(
|
||||
'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
|
||||
"openlibraryKey", remote_field="key", formatter=get_openlibrary_key
|
||||
),
|
||||
Mapping("goodreadsKey", remote_field="goodreads_key"),
|
||||
Mapping("asin"),
|
||||
Mapping(
|
||||
'openlibraryKey', remote_field='key',
|
||||
formatter=get_openlibrary_key
|
||||
"firstPublishedDate",
|
||||
remote_field="first_publish_date",
|
||||
),
|
||||
Mapping('goodreadsKey', remote_field='goodreads_key'),
|
||||
Mapping('asin'),
|
||||
Mapping(
|
||||
'firstPublishedDate', remote_field='first_publish_date',
|
||||
),
|
||||
Mapping('publishedDate', remote_field='publish_date'),
|
||||
Mapping('pages', remote_field='number_of_pages'),
|
||||
Mapping('physicalFormat', remote_field='physical_format'),
|
||||
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 = [
|
||||
Mapping('id', remote_field='key', formatter=get_remote_id),
|
||||
Mapping('name'),
|
||||
Mapping("id", remote_field="key", formatter=get_remote_id),
|
||||
Mapping("name"),
|
||||
Mapping(
|
||||
'openlibraryKey', remote_field='key',
|
||||
formatter=get_openlibrary_key
|
||||
"openlibraryKey", remote_field="key", formatter=get_openlibrary_key
|
||||
),
|
||||
Mapping('born', remote_field='birth_date'),
|
||||
Mapping('died', remote_field='death_date'),
|
||||
Mapping('bio', formatter=get_description),
|
||||
Mapping("born", remote_field="birth_date"),
|
||||
Mapping("died", remote_field="death_date"),
|
||||
Mapping("bio", formatter=get_description),
|
||||
]
|
||||
|
||||
|
||||
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:
|
||||
key = data['key']
|
||||
key = data["key"]
|
||||
except KeyError:
|
||||
raise ConnectorException('Invalid book data')
|
||||
return '%s%s' % (self.books_url, key)
|
||||
|
||||
raise ConnectorException("Invalid book data")
|
||||
return "%s%s" % (self.books_url, key)
|
||||
|
||||
def is_work_data(self, data):
|
||||
return bool(re.match(r'^[\/\w]+OL\d+W$', data['key']))
|
||||
|
||||
return bool(re.match(r"^[\/\w]+OL\d+W$", data["key"]))
|
||||
|
||||
def get_edition_from_work_data(self, data):
|
||||
try:
|
||||
key = data['key']
|
||||
key = data["key"]
|
||||
except KeyError:
|
||||
raise ConnectorException('Invalid book data')
|
||||
url = '%s%s/editions' % (self.books_url, key)
|
||||
raise ConnectorException("Invalid book data")
|
||||
url = "%s%s/editions" % (self.books_url, key)
|
||||
data = get_data(url)
|
||||
return pick_default_edition(data['entries'])
|
||||
|
||||
return pick_default_edition(data["entries"])
|
||||
|
||||
def get_work_from_edition_data(self, data):
|
||||
try:
|
||||
key = data['works'][0]['key']
|
||||
key = data["works"][0]["key"]
|
||||
except (IndexError, KeyError):
|
||||
raise ConnectorException('No work found for edition')
|
||||
url = '%s%s' % (self.books_url, key)
|
||||
raise ConnectorException("No work found for edition")
|
||||
url = "%s%s" % (self.books_url, key)
|
||||
return get_data(url)
|
||||
|
||||
|
||||
def get_authors_from_data(self, data):
|
||||
''' parse author json and load or create authors '''
|
||||
for author_blob in data.get('authors', []):
|
||||
author_blob = author_blob.get('author', author_blob)
|
||||
""" parse author json and load or create authors """
|
||||
for author_blob in data.get("authors", []):
|
||||
author_blob = author_blob.get("author", author_blob)
|
||||
# this id is "/authors/OL1234567A"
|
||||
author_id = author_blob['key']
|
||||
url = '%s%s' % (self.base_url, author_id)
|
||||
yield self.get_or_create_author(url)
|
||||
author_id = author_blob["key"]
|
||||
url = "%s%s" % (self.base_url, author_id)
|
||||
author = self.get_or_create_author(url)
|
||||
if not author:
|
||||
continue
|
||||
yield author
|
||||
|
||||
|
||||
def get_cover_url(self, cover_blob):
|
||||
''' ask openlibrary for the cover '''
|
||||
def get_cover_url(self, cover_blob, size="L"):
|
||||
""" ask openlibrary for the cover """
|
||||
if not cover_blob:
|
||||
return None
|
||||
cover_id = cover_blob[0]
|
||||
image_name = '%s-L.jpg' % cover_id
|
||||
return '%s/b/id/%s' % (self.covers_url, image_name)
|
||||
|
||||
image_name = "%s-%s.jpg" % (cover_id, size)
|
||||
return "%s/b/id/%s" % (self.covers_url, image_name)
|
||||
|
||||
def parse_search_data(self, data):
|
||||
return data.get('docs')
|
||||
|
||||
return data.get("docs")
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
# build the remote id from the openlibrary key
|
||||
key = self.books_url + search_result['key']
|
||||
author = search_result.get('author_name') or ['Unknown']
|
||||
key = self.books_url + search_result["key"]
|
||||
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(
|
||||
title=search_result.get('title'),
|
||||
title=search_result.get("title"),
|
||||
key=key,
|
||||
author=', '.join(author),
|
||||
author=", ".join(author),
|
||||
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):
|
||||
''' query openlibrary for editions of a work '''
|
||||
url = '%s/works/%s/editions' % (self.books_url, olkey)
|
||||
""" query openlibrary for editions of a work """
|
||||
url = "%s/works/%s/editions" % (self.books_url, olkey)
|
||||
return get_data(url)
|
||||
|
||||
|
||||
def expand_book_data(self, book):
|
||||
work = book
|
||||
# go from the edition to the work, if necessary
|
||||
|
@ -148,7 +158,7 @@ class Connector(AbstractConnector):
|
|||
# who knows, man
|
||||
return
|
||||
|
||||
for edition_data in edition_options.get('entries'):
|
||||
for edition_data in edition_options.get("entries"):
|
||||
# does this edition have ANY interesting data?
|
||||
if ignore_edition(edition_data):
|
||||
continue
|
||||
|
@ -156,62 +166,59 @@ class Connector(AbstractConnector):
|
|||
|
||||
|
||||
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
|
||||
if edition_data.get('isbn_13') or edition_data.get('isbn_10'):
|
||||
print(edition_data.get('isbn_10'))
|
||||
if edition_data.get("isbn_13") or edition_data.get("isbn_10"):
|
||||
return False
|
||||
# grudgingly, oclc can stay
|
||||
if edition_data.get('oclc_numbers'):
|
||||
print(edition_data.get('oclc_numbers'))
|
||||
if edition_data.get("oclc_numbers"):
|
||||
return False
|
||||
# if it has a cover it can stay
|
||||
if edition_data.get('covers'):
|
||||
print(edition_data.get('covers'))
|
||||
if edition_data.get("covers"):
|
||||
return False
|
||||
# keep non-english editions
|
||||
if edition_data.get('languages') and \
|
||||
'languages/eng' not in str(edition_data.get('languages')):
|
||||
print(edition_data.get('languages'))
|
||||
if edition_data.get("languages") and "languages/eng" not in str(
|
||||
edition_data.get("languages")
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
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):
|
||||
return description_blob.get('value')
|
||||
return description_blob.get("value")
|
||||
return description_blob
|
||||
|
||||
|
||||
def get_openlibrary_key(key):
|
||||
''' convert /books/OL27320736M into OL27320736M '''
|
||||
return key.split('/')[-1]
|
||||
""" convert /books/OL27320736M into OL27320736M """
|
||||
return key.split("/")[-1]
|
||||
|
||||
|
||||
def get_languages(language_blob):
|
||||
''' /language/eng -> English '''
|
||||
""" /language/eng -> English """
|
||||
langs = []
|
||||
for lang in language_blob:
|
||||
langs.append(
|
||||
languages.get(lang.get('key', ''), None)
|
||||
)
|
||||
langs.append(languages.get(lang.get("key", ""), None))
|
||||
return langs
|
||||
|
||||
|
||||
def pick_default_edition(options):
|
||||
''' favor physical copies with covers in english '''
|
||||
""" favor physical copies with covers in english """
|
||||
if not options:
|
||||
return None
|
||||
if len(options) == 1:
|
||||
return options[0]
|
||||
|
||||
options = [e for e in options if e.get('covers')] or options
|
||||
options = [e for e in options if \
|
||||
'/languages/eng' in str(e.get('languages'))] or options
|
||||
formats = ['paperback', 'hardcover', 'mass market paperback']
|
||||
options = [e for e in options if \
|
||||
str(e.get('physical_format')).lower() in formats] 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
|
||||
options = [e for e in options if e.get("covers")] or options
|
||||
options = [
|
||||
e for e in options if "/languages/eng" in str(e.get("languages"))
|
||||
] or options
|
||||
formats = ["paperback", "hardcover", "mass market paperback"]
|
||||
options = [
|
||||
e for e in options if str(e.get("physical_format")).lower() in formats
|
||||
] 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]
|
||||
|
|
|
@ -1,467 +1,467 @@
|
|||
''' key lookups for openlibrary languages '''
|
||||
""" key lookups for openlibrary languages """
|
||||
languages = {
|
||||
'/languages/eng': 'English',
|
||||
'/languages/fre': 'French',
|
||||
'/languages/spa': 'Spanish',
|
||||
'/languages/ger': 'German',
|
||||
'/languages/rus': 'Russian',
|
||||
'/languages/ita': 'Italian',
|
||||
'/languages/chi': 'Chinese',
|
||||
'/languages/jpn': 'Japanese',
|
||||
'/languages/por': 'Portuguese',
|
||||
'/languages/ara': 'Arabic',
|
||||
'/languages/pol': 'Polish',
|
||||
'/languages/heb': 'Hebrew',
|
||||
'/languages/kor': 'Korean',
|
||||
'/languages/dut': 'Dutch',
|
||||
'/languages/ind': 'Indonesian',
|
||||
'/languages/lat': 'Latin',
|
||||
'/languages/und': 'Undetermined',
|
||||
'/languages/cmn': 'Mandarin',
|
||||
'/languages/hin': 'Hindi',
|
||||
'/languages/swe': 'Swedish',
|
||||
'/languages/dan': 'Danish',
|
||||
'/languages/urd': 'Urdu',
|
||||
'/languages/hun': 'Hungarian',
|
||||
'/languages/cze': 'Czech',
|
||||
'/languages/tur': 'Turkish',
|
||||
'/languages/ukr': 'Ukrainian',
|
||||
'/languages/gre': 'Greek',
|
||||
'/languages/vie': 'Vietnamese',
|
||||
'/languages/bul': 'Bulgarian',
|
||||
'/languages/ben': 'Bengali',
|
||||
'/languages/rum': 'Romanian',
|
||||
'/languages/cat': 'Catalan',
|
||||
'/languages/nor': 'Norwegian',
|
||||
'/languages/tha': 'Thai',
|
||||
'/languages/per': 'Persian',
|
||||
'/languages/scr': 'Croatian',
|
||||
'/languages/mul': 'Multiple languages',
|
||||
'/languages/fin': 'Finnish',
|
||||
'/languages/tam': 'Tamil',
|
||||
'/languages/guj': 'Gujarati',
|
||||
'/languages/mar': 'Marathi',
|
||||
'/languages/scc': 'Serbian',
|
||||
'/languages/pan': 'Panjabi',
|
||||
'/languages/wel': 'Welsh',
|
||||
'/languages/tel': 'Telugu',
|
||||
'/languages/yid': 'Yiddish',
|
||||
'/languages/kan': 'Kannada',
|
||||
'/languages/slo': 'Slovak',
|
||||
'/languages/san': 'Sanskrit',
|
||||
'/languages/arm': 'Armenian',
|
||||
'/languages/mal': 'Malayalam',
|
||||
'/languages/may': 'Malay',
|
||||
'/languages/bur': 'Burmese',
|
||||
'/languages/slv': 'Slovenian',
|
||||
'/languages/lit': 'Lithuanian',
|
||||
'/languages/tib': 'Tibetan',
|
||||
'/languages/lav': 'Latvian',
|
||||
'/languages/est': 'Estonian',
|
||||
'/languages/nep': 'Nepali',
|
||||
'/languages/ori': 'Oriya',
|
||||
'/languages/mon': 'Mongolian',
|
||||
'/languages/alb': 'Albanian',
|
||||
'/languages/iri': 'Irish',
|
||||
'/languages/geo': 'Georgian',
|
||||
'/languages/afr': 'Afrikaans',
|
||||
'/languages/grc': 'Ancient Greek',
|
||||
'/languages/mac': 'Macedonian',
|
||||
'/languages/bel': 'Belarusian',
|
||||
'/languages/ice': 'Icelandic',
|
||||
'/languages/srp': 'Serbian',
|
||||
'/languages/snh': 'Sinhalese',
|
||||
'/languages/snd': 'Sindhi',
|
||||
'/languages/ota': 'Turkish, Ottoman',
|
||||
'/languages/kur': 'Kurdish',
|
||||
'/languages/aze': 'Azerbaijani',
|
||||
'/languages/pus': 'Pushto',
|
||||
'/languages/amh': 'Amharic',
|
||||
'/languages/gag': 'Galician',
|
||||
'/languages/hrv': 'Croatian',
|
||||
'/languages/sin': 'Sinhalese',
|
||||
'/languages/asm': 'Assamese',
|
||||
'/languages/uzb': 'Uzbek',
|
||||
'/languages/gae': 'Scottish Gaelix',
|
||||
'/languages/kaz': 'Kazakh',
|
||||
'/languages/swa': 'Swahili',
|
||||
'/languages/bos': 'Bosnian',
|
||||
'/languages/glg': 'Galician ',
|
||||
'/languages/baq': 'Basque',
|
||||
'/languages/tgl': 'Tagalog',
|
||||
'/languages/raj': 'Rajasthani',
|
||||
'/languages/gle': 'Irish',
|
||||
'/languages/lao': 'Lao',
|
||||
'/languages/jav': 'Javanese',
|
||||
'/languages/mai': 'Maithili',
|
||||
'/languages/tgk': 'Tajik ',
|
||||
'/languages/khm': 'Khmer',
|
||||
'/languages/roh': 'Raeto-Romance',
|
||||
'/languages/kok': 'Konkani ',
|
||||
'/languages/sit': 'Sino-Tibetan (Other)',
|
||||
'/languages/mol': 'Moldavian',
|
||||
'/languages/kir': 'Kyrgyz',
|
||||
'/languages/new': 'Newari',
|
||||
'/languages/inc': 'Indic (Other)',
|
||||
'/languages/frm': 'French, Middle (ca. 1300-1600)',
|
||||
'/languages/esp': 'Esperanto',
|
||||
'/languages/hau': 'Hausa',
|
||||
'/languages/tag': 'Tagalog',
|
||||
'/languages/tuk': 'Turkmen',
|
||||
'/languages/enm': 'English, Middle (1100-1500)',
|
||||
'/languages/map': 'Austronesian (Other)',
|
||||
'/languages/pli': 'Pali',
|
||||
'/languages/fro': 'French, Old (ca. 842-1300)',
|
||||
'/languages/nic': 'Niger-Kordofanian (Other)',
|
||||
'/languages/tir': 'Tigrinya',
|
||||
'/languages/wen': 'Sorbian (Other)',
|
||||
'/languages/bho': 'Bhojpuri',
|
||||
'/languages/roa': 'Romance (Other)',
|
||||
'/languages/tut': 'Altaic (Other)',
|
||||
'/languages/bra': 'Braj',
|
||||
'/languages/sun': 'Sundanese',
|
||||
'/languages/fiu': 'Finno-Ugrian (Other)',
|
||||
'/languages/far': 'Faroese',
|
||||
'/languages/ban': 'Balinese',
|
||||
'/languages/tar': 'Tatar',
|
||||
'/languages/bak': 'Bashkir',
|
||||
'/languages/tat': 'Tatar',
|
||||
'/languages/chu': 'Church Slavic',
|
||||
'/languages/dra': 'Dravidian (Other)',
|
||||
'/languages/pra': 'Prakrit languages',
|
||||
'/languages/paa': 'Papuan (Other)',
|
||||
'/languages/doi': 'Dogri',
|
||||
'/languages/lah': 'Lahndā',
|
||||
'/languages/mni': 'Manipuri',
|
||||
'/languages/yor': 'Yoruba',
|
||||
'/languages/gmh': 'German, Middle High (ca. 1050-1500)',
|
||||
'/languages/kas': 'Kashmiri',
|
||||
'/languages/fri': 'Frisian',
|
||||
'/languages/mla': 'Malagasy',
|
||||
'/languages/egy': 'Egyptian',
|
||||
'/languages/rom': 'Romani',
|
||||
'/languages/syr': 'Syriac, Modern',
|
||||
'/languages/cau': 'Caucasian (Other)',
|
||||
'/languages/hbs': 'Serbo-Croatian',
|
||||
'/languages/sai': 'South American Indian (Other)',
|
||||
'/languages/pro': 'Provençal (to 1500)',
|
||||
'/languages/cpf': 'Creoles and Pidgins, French-based (Other)',
|
||||
'/languages/ang': 'English, Old (ca. 450-1100)',
|
||||
'/languages/bal': 'Baluchi',
|
||||
'/languages/gla': 'Scottish Gaelic',
|
||||
'/languages/chv': 'Chuvash',
|
||||
'/languages/kin': 'Kinyarwanda',
|
||||
'/languages/zul': 'Zulu',
|
||||
'/languages/sla': 'Slavic (Other)',
|
||||
'/languages/som': 'Somali',
|
||||
'/languages/mlt': 'Maltese',
|
||||
'/languages/uig': 'Uighur',
|
||||
'/languages/mlg': 'Malagasy',
|
||||
'/languages/sho': 'Shona',
|
||||
'/languages/lan': 'Occitan (post 1500)',
|
||||
'/languages/bre': 'Breton',
|
||||
'/languages/sco': 'Scots',
|
||||
'/languages/sso': 'Sotho',
|
||||
'/languages/myn': 'Mayan languages',
|
||||
'/languages/xho': 'Xhosa',
|
||||
'/languages/gem': 'Germanic (Other)',
|
||||
'/languages/esk': 'Eskimo languages',
|
||||
'/languages/akk': 'Akkadian',
|
||||
'/languages/div': 'Maldivian',
|
||||
'/languages/sah': 'Yakut',
|
||||
'/languages/tsw': 'Tswana',
|
||||
'/languages/nso': 'Northern Sotho',
|
||||
'/languages/pap': 'Papiamento',
|
||||
'/languages/bnt': 'Bantu (Other)',
|
||||
'/languages/oss': 'Ossetic',
|
||||
'/languages/cre': 'Cree',
|
||||
'/languages/ibo': 'Igbo',
|
||||
'/languages/fao': 'Faroese',
|
||||
'/languages/nai': 'North American Indian (Other)',
|
||||
'/languages/mag': 'Magahi',
|
||||
'/languages/arc': 'Aramaic',
|
||||
'/languages/epo': 'Esperanto',
|
||||
'/languages/kha': 'Khasi',
|
||||
'/languages/oji': 'Ojibwa',
|
||||
'/languages/que': 'Quechua',
|
||||
'/languages/lug': 'Ganda',
|
||||
'/languages/mwr': 'Marwari',
|
||||
'/languages/awa': 'Awadhi ',
|
||||
'/languages/cor': 'Cornish',
|
||||
'/languages/lad': 'Ladino',
|
||||
'/languages/dzo': 'Dzongkha',
|
||||
'/languages/cop': 'Coptic',
|
||||
'/languages/nah': 'Nahuatl',
|
||||
'/languages/cai': 'Central American Indian (Other)',
|
||||
'/languages/phi': 'Philippine (Other)',
|
||||
'/languages/moh': 'Mohawk',
|
||||
'/languages/crp': 'Creoles and Pidgins (Other)',
|
||||
'/languages/nya': 'Nyanja',
|
||||
'/languages/wol': 'Wolof ',
|
||||
'/languages/haw': 'Hawaiian',
|
||||
'/languages/eth': 'Ethiopic',
|
||||
'/languages/mis': 'Miscellaneous languages',
|
||||
'/languages/mkh': 'Mon-Khmer (Other)',
|
||||
'/languages/alg': 'Algonquian (Other)',
|
||||
'/languages/nde': 'Ndebele (Zimbabwe)',
|
||||
'/languages/ssa': 'Nilo-Saharan (Other)',
|
||||
'/languages/chm': 'Mari',
|
||||
'/languages/che': 'Chechen',
|
||||
'/languages/gez': 'Ethiopic',
|
||||
'/languages/ven': 'Venda',
|
||||
'/languages/cam': 'Khmer',
|
||||
'/languages/fur': 'Friulian',
|
||||
'/languages/ful': 'Fula',
|
||||
'/languages/gal': 'Oromo',
|
||||
'/languages/jrb': 'Judeo-Arabic',
|
||||
'/languages/bua': 'Buriat',
|
||||
'/languages/ady': 'Adygei',
|
||||
'/languages/bem': 'Bemba',
|
||||
'/languages/kar': 'Karen languages',
|
||||
'/languages/sna': 'Shona',
|
||||
'/languages/twi': 'Twi',
|
||||
'/languages/btk': 'Batak',
|
||||
'/languages/kaa': 'Kara-Kalpak',
|
||||
'/languages/kom': 'Komi',
|
||||
'/languages/sot': 'Sotho',
|
||||
'/languages/tso': 'Tsonga',
|
||||
'/languages/cpe': 'Creoles and Pidgins, English-based (Other)',
|
||||
'/languages/gua': 'Guarani',
|
||||
'/languages/mao': 'Maori',
|
||||
'/languages/mic': 'Micmac',
|
||||
'/languages/swz': 'Swazi',
|
||||
'/languages/taj': 'Tajik',
|
||||
'/languages/smo': 'Samoan',
|
||||
'/languages/ace': 'Achinese',
|
||||
'/languages/afa': 'Afroasiatic (Other)',
|
||||
'/languages/lap': 'Sami',
|
||||
'/languages/min': 'Minangkabau',
|
||||
'/languages/oci': 'Occitan (post 1500)',
|
||||
'/languages/tsn': 'Tswana',
|
||||
'/languages/pal': 'Pahlavi',
|
||||
'/languages/sux': 'Sumerian',
|
||||
'/languages/ewe': 'Ewe',
|
||||
'/languages/him': 'Himachali',
|
||||
'/languages/kaw': 'Kawi',
|
||||
'/languages/lus': 'Lushai',
|
||||
'/languages/ceb': 'Cebuano',
|
||||
'/languages/chr': 'Cherokee',
|
||||
'/languages/fil': 'Filipino',
|
||||
'/languages/ndo': 'Ndonga',
|
||||
'/languages/ilo': 'Iloko',
|
||||
'/languages/kbd': 'Kabardian',
|
||||
'/languages/orm': 'Oromo',
|
||||
'/languages/dum': 'Dutch, Middle (ca. 1050-1350)',
|
||||
'/languages/bam': 'Bambara',
|
||||
'/languages/goh': 'Old High German',
|
||||
'/languages/got': 'Gothic',
|
||||
'/languages/kon': 'Kongo',
|
||||
'/languages/mun': 'Munda (Other)',
|
||||
'/languages/kru': 'Kurukh',
|
||||
'/languages/pam': 'Pampanga',
|
||||
'/languages/grn': 'Guarani',
|
||||
'/languages/gaa': 'Gã',
|
||||
'/languages/fry': 'Frisian',
|
||||
'/languages/iba': 'Iban',
|
||||
'/languages/mak': 'Makasar',
|
||||
'/languages/kik': 'Kikuyu',
|
||||
'/languages/cho': 'Choctaw',
|
||||
'/languages/cpp': 'Creoles and Pidgins, Portuguese-based (Other)',
|
||||
'/languages/dak': 'Dakota',
|
||||
'/languages/udm': 'Udmurt ',
|
||||
'/languages/hat': 'Haitian French Creole',
|
||||
'/languages/mus': 'Creek',
|
||||
'/languages/ber': 'Berber (Other)',
|
||||
'/languages/hil': 'Hiligaynon',
|
||||
'/languages/iro': 'Iroquoian (Other)',
|
||||
'/languages/kua': 'Kuanyama',
|
||||
'/languages/mno': 'Manobo languages',
|
||||
'/languages/run': 'Rundi',
|
||||
'/languages/sat': 'Santali',
|
||||
'/languages/shn': 'Shan',
|
||||
'/languages/tyv': 'Tuvinian',
|
||||
'/languages/chg': 'Chagatai',
|
||||
'/languages/syc': 'Syriac',
|
||||
'/languages/ath': 'Athapascan (Other)',
|
||||
'/languages/aym': 'Aymara',
|
||||
'/languages/bug': 'Bugis',
|
||||
'/languages/cel': 'Celtic (Other)',
|
||||
'/languages/int': 'Interlingua (International Auxiliary Language Association)',
|
||||
'/languages/xal': 'Oirat',
|
||||
'/languages/ava': 'Avaric',
|
||||
'/languages/son': 'Songhai',
|
||||
'/languages/tah': 'Tahitian',
|
||||
'/languages/tet': 'Tetum',
|
||||
'/languages/ira': 'Iranian (Other)',
|
||||
'/languages/kac': 'Kachin',
|
||||
'/languages/nob': 'Norwegian (Bokmål)',
|
||||
'/languages/vai': 'Vai',
|
||||
'/languages/bik': 'Bikol',
|
||||
'/languages/mos': 'Mooré',
|
||||
'/languages/tig': 'Tigré',
|
||||
'/languages/fat': 'Fanti',
|
||||
'/languages/her': 'Herero',
|
||||
'/languages/kal': 'Kalâtdlisut',
|
||||
'/languages/mad': 'Madurese',
|
||||
'/languages/yue': 'Cantonese',
|
||||
'/languages/chn': 'Chinook jargon',
|
||||
'/languages/hmn': 'Hmong',
|
||||
'/languages/lin': 'Lingala',
|
||||
'/languages/man': 'Mandingo',
|
||||
'/languages/nds': 'Low German',
|
||||
'/languages/bas': 'Basa',
|
||||
'/languages/gay': 'Gayo',
|
||||
'/languages/gsw': 'gsw',
|
||||
'/languages/ine': 'Indo-European (Other)',
|
||||
'/languages/kro': 'Kru (Other)',
|
||||
'/languages/kum': 'Kumyk',
|
||||
'/languages/tsi': 'Tsimshian',
|
||||
'/languages/zap': 'Zapotec',
|
||||
'/languages/ach': 'Acoli',
|
||||
'/languages/ada': 'Adangme',
|
||||
'/languages/aka': 'Akan',
|
||||
'/languages/khi': 'Khoisan (Other)',
|
||||
'/languages/srd': 'Sardinian',
|
||||
'/languages/arn': 'Mapuche',
|
||||
'/languages/dyu': 'Dyula',
|
||||
'/languages/loz': 'Lozi',
|
||||
'/languages/ltz': 'Luxembourgish',
|
||||
'/languages/sag': 'Sango (Ubangi Creole)',
|
||||
'/languages/lez': 'Lezgian',
|
||||
'/languages/luo': 'Luo (Kenya and Tanzania)',
|
||||
'/languages/ssw': 'Swazi ',
|
||||
'/languages/krc': 'Karachay-Balkar',
|
||||
'/languages/nyn': 'Nyankole',
|
||||
'/languages/sal': 'Salishan languages',
|
||||
'/languages/jpr': 'Judeo-Persian',
|
||||
'/languages/pau': 'Palauan',
|
||||
'/languages/smi': 'Sami',
|
||||
'/languages/aar': 'Afar',
|
||||
'/languages/abk': 'Abkhaz',
|
||||
'/languages/gon': 'Gondi',
|
||||
'/languages/nzi': 'Nzima',
|
||||
'/languages/sam': 'Samaritan Aramaic',
|
||||
'/languages/sao': 'Samoan',
|
||||
'/languages/srr': 'Serer',
|
||||
'/languages/apa': 'Apache languages',
|
||||
'/languages/crh': 'Crimean Tatar',
|
||||
'/languages/efi': 'Efik',
|
||||
'/languages/iku': 'Inuktitut',
|
||||
'/languages/nav': 'Navajo',
|
||||
'/languages/pon': 'Ponape',
|
||||
'/languages/tmh': 'Tamashek',
|
||||
'/languages/aus': 'Australian languages',
|
||||
'/languages/oto': 'Otomian languages',
|
||||
'/languages/war': 'Waray',
|
||||
'/languages/ypk': 'Yupik languages',
|
||||
'/languages/ave': 'Avestan',
|
||||
'/languages/cus': 'Cushitic (Other)',
|
||||
'/languages/del': 'Delaware',
|
||||
'/languages/fon': 'Fon',
|
||||
'/languages/ina': 'Interlingua (International Auxiliary Language Association)',
|
||||
'/languages/myv': 'Erzya',
|
||||
'/languages/pag': 'Pangasinan',
|
||||
'/languages/peo': 'Old Persian (ca. 600-400 B.C.)',
|
||||
'/languages/vls': 'Flemish',
|
||||
'/languages/bai': 'Bamileke languages',
|
||||
'/languages/bla': 'Siksika',
|
||||
'/languages/day': 'Dayak',
|
||||
'/languages/men': 'Mende',
|
||||
'/languages/tai': 'Tai',
|
||||
'/languages/ton': 'Tongan',
|
||||
'/languages/uga': 'Ugaritic',
|
||||
'/languages/yao': 'Yao (Africa)',
|
||||
'/languages/zza': 'Zaza',
|
||||
'/languages/bin': 'Edo',
|
||||
'/languages/frs': 'East Frisian',
|
||||
'/languages/inh': 'Ingush',
|
||||
'/languages/mah': 'Marshallese',
|
||||
'/languages/sem': 'Semitic (Other)',
|
||||
'/languages/art': 'Artificial (Other)',
|
||||
'/languages/chy': 'Cheyenne',
|
||||
'/languages/cmc': 'Chamic languages',
|
||||
'/languages/dar': 'Dargwa',
|
||||
'/languages/dua': 'Duala',
|
||||
'/languages/elx': 'Elamite',
|
||||
'/languages/fan': 'Fang',
|
||||
'/languages/fij': 'Fijian',
|
||||
'/languages/gil': 'Gilbertese',
|
||||
'/languages/ijo': 'Ijo',
|
||||
'/languages/kam': 'Kamba',
|
||||
'/languages/nog': 'Nogai',
|
||||
'/languages/non': 'Old Norse',
|
||||
'/languages/tem': 'Temne',
|
||||
'/languages/arg': 'Aragonese',
|
||||
'/languages/arp': 'Arapaho',
|
||||
'/languages/arw': 'Arawak',
|
||||
'/languages/din': 'Dinka',
|
||||
'/languages/grb': 'Grebo',
|
||||
'/languages/kos': 'Kusaie',
|
||||
'/languages/lub': 'Luba-Katanga',
|
||||
'/languages/mnc': 'Manchu',
|
||||
'/languages/nyo': 'Nyoro',
|
||||
'/languages/rar': 'Rarotongan',
|
||||
'/languages/sel': 'Selkup',
|
||||
'/languages/tkl': 'Tokelauan',
|
||||
'/languages/tog': 'Tonga (Nyasa)',
|
||||
'/languages/tum': 'Tumbuka',
|
||||
'/languages/alt': 'Altai',
|
||||
'/languages/ase': 'American Sign Language',
|
||||
'/languages/ast': 'Asturian',
|
||||
'/languages/chk': 'Chuukese',
|
||||
'/languages/cos': 'Corsican',
|
||||
'/languages/ewo': 'Ewondo',
|
||||
'/languages/gor': 'Gorontalo',
|
||||
'/languages/hmo': 'Hiri Motu',
|
||||
'/languages/lol': 'Mongo-Nkundu',
|
||||
'/languages/lun': 'Lunda',
|
||||
'/languages/mas': 'Masai',
|
||||
'/languages/niu': 'Niuean',
|
||||
'/languages/rup': 'Aromanian',
|
||||
'/languages/sas': 'Sasak',
|
||||
'/languages/sio': 'Siouan (Other)',
|
||||
'/languages/sus': 'Susu',
|
||||
'/languages/zun': 'Zuni',
|
||||
'/languages/bat': 'Baltic (Other)',
|
||||
'/languages/car': 'Carib',
|
||||
'/languages/cha': 'Chamorro',
|
||||
'/languages/kab': 'Kabyle',
|
||||
'/languages/kau': 'Kanuri',
|
||||
'/languages/kho': 'Khotanese',
|
||||
'/languages/lua': 'Luba-Lulua',
|
||||
'/languages/mdf': 'Moksha',
|
||||
'/languages/nbl': 'Ndebele (South Africa)',
|
||||
'/languages/umb': 'Umbundu',
|
||||
'/languages/wak': 'Wakashan languages',
|
||||
'/languages/wal': 'Wolayta',
|
||||
'/languages/ale': 'Aleut',
|
||||
'/languages/bis': 'Bislama',
|
||||
'/languages/gba': 'Gbaya',
|
||||
'/languages/glv': 'Manx',
|
||||
'/languages/gul': 'Gullah',
|
||||
'/languages/ipk': 'Inupiaq',
|
||||
'/languages/krl': 'Karelian',
|
||||
'/languages/lam': 'Lamba (Zambia and Congo)',
|
||||
'/languages/sad': 'Sandawe',
|
||||
'/languages/sid': 'Sidamo',
|
||||
'/languages/snk': 'Soninke',
|
||||
'/languages/srn': 'Sranan',
|
||||
'/languages/suk': 'Sukuma',
|
||||
'/languages/ter': 'Terena',
|
||||
'/languages/tiv': 'Tiv',
|
||||
'/languages/tli': 'Tlingit',
|
||||
'/languages/tpi': 'Tok Pisin',
|
||||
'/languages/tvl': 'Tuvaluan',
|
||||
'/languages/yap': 'Yapese',
|
||||
'/languages/eka': 'Ekajuk',
|
||||
'/languages/hsb': 'Upper Sorbian',
|
||||
'/languages/ido': 'Ido',
|
||||
'/languages/kmb': 'Kimbundu',
|
||||
'/languages/kpe': 'Kpelle',
|
||||
'/languages/mwl': 'Mirandese',
|
||||
'/languages/nno': 'Nynorsk',
|
||||
'/languages/nub': 'Nubian languages',
|
||||
'/languages/osa': 'Osage',
|
||||
'/languages/sme': 'Northern Sami',
|
||||
'/languages/znd': 'Zande languages',
|
||||
"/languages/eng": "English",
|
||||
"/languages/fre": "French",
|
||||
"/languages/spa": "Spanish",
|
||||
"/languages/ger": "German",
|
||||
"/languages/rus": "Russian",
|
||||
"/languages/ita": "Italian",
|
||||
"/languages/chi": "Chinese",
|
||||
"/languages/jpn": "Japanese",
|
||||
"/languages/por": "Portuguese",
|
||||
"/languages/ara": "Arabic",
|
||||
"/languages/pol": "Polish",
|
||||
"/languages/heb": "Hebrew",
|
||||
"/languages/kor": "Korean",
|
||||
"/languages/dut": "Dutch",
|
||||
"/languages/ind": "Indonesian",
|
||||
"/languages/lat": "Latin",
|
||||
"/languages/und": "Undetermined",
|
||||
"/languages/cmn": "Mandarin",
|
||||
"/languages/hin": "Hindi",
|
||||
"/languages/swe": "Swedish",
|
||||
"/languages/dan": "Danish",
|
||||
"/languages/urd": "Urdu",
|
||||
"/languages/hun": "Hungarian",
|
||||
"/languages/cze": "Czech",
|
||||
"/languages/tur": "Turkish",
|
||||
"/languages/ukr": "Ukrainian",
|
||||
"/languages/gre": "Greek",
|
||||
"/languages/vie": "Vietnamese",
|
||||
"/languages/bul": "Bulgarian",
|
||||
"/languages/ben": "Bengali",
|
||||
"/languages/rum": "Romanian",
|
||||
"/languages/cat": "Catalan",
|
||||
"/languages/nor": "Norwegian",
|
||||
"/languages/tha": "Thai",
|
||||
"/languages/per": "Persian",
|
||||
"/languages/scr": "Croatian",
|
||||
"/languages/mul": "Multiple languages",
|
||||
"/languages/fin": "Finnish",
|
||||
"/languages/tam": "Tamil",
|
||||
"/languages/guj": "Gujarati",
|
||||
"/languages/mar": "Marathi",
|
||||
"/languages/scc": "Serbian",
|
||||
"/languages/pan": "Panjabi",
|
||||
"/languages/wel": "Welsh",
|
||||
"/languages/tel": "Telugu",
|
||||
"/languages/yid": "Yiddish",
|
||||
"/languages/kan": "Kannada",
|
||||
"/languages/slo": "Slovak",
|
||||
"/languages/san": "Sanskrit",
|
||||
"/languages/arm": "Armenian",
|
||||
"/languages/mal": "Malayalam",
|
||||
"/languages/may": "Malay",
|
||||
"/languages/bur": "Burmese",
|
||||
"/languages/slv": "Slovenian",
|
||||
"/languages/lit": "Lithuanian",
|
||||
"/languages/tib": "Tibetan",
|
||||
"/languages/lav": "Latvian",
|
||||
"/languages/est": "Estonian",
|
||||
"/languages/nep": "Nepali",
|
||||
"/languages/ori": "Oriya",
|
||||
"/languages/mon": "Mongolian",
|
||||
"/languages/alb": "Albanian",
|
||||
"/languages/iri": "Irish",
|
||||
"/languages/geo": "Georgian",
|
||||
"/languages/afr": "Afrikaans",
|
||||
"/languages/grc": "Ancient Greek",
|
||||
"/languages/mac": "Macedonian",
|
||||
"/languages/bel": "Belarusian",
|
||||
"/languages/ice": "Icelandic",
|
||||
"/languages/srp": "Serbian",
|
||||
"/languages/snh": "Sinhalese",
|
||||
"/languages/snd": "Sindhi",
|
||||
"/languages/ota": "Turkish, Ottoman",
|
||||
"/languages/kur": "Kurdish",
|
||||
"/languages/aze": "Azerbaijani",
|
||||
"/languages/pus": "Pushto",
|
||||
"/languages/amh": "Amharic",
|
||||
"/languages/gag": "Galician",
|
||||
"/languages/hrv": "Croatian",
|
||||
"/languages/sin": "Sinhalese",
|
||||
"/languages/asm": "Assamese",
|
||||
"/languages/uzb": "Uzbek",
|
||||
"/languages/gae": "Scottish Gaelix",
|
||||
"/languages/kaz": "Kazakh",
|
||||
"/languages/swa": "Swahili",
|
||||
"/languages/bos": "Bosnian",
|
||||
"/languages/glg": "Galician ",
|
||||
"/languages/baq": "Basque",
|
||||
"/languages/tgl": "Tagalog",
|
||||
"/languages/raj": "Rajasthani",
|
||||
"/languages/gle": "Irish",
|
||||
"/languages/lao": "Lao",
|
||||
"/languages/jav": "Javanese",
|
||||
"/languages/mai": "Maithili",
|
||||
"/languages/tgk": "Tajik ",
|
||||
"/languages/khm": "Khmer",
|
||||
"/languages/roh": "Raeto-Romance",
|
||||
"/languages/kok": "Konkani ",
|
||||
"/languages/sit": "Sino-Tibetan (Other)",
|
||||
"/languages/mol": "Moldavian",
|
||||
"/languages/kir": "Kyrgyz",
|
||||
"/languages/new": "Newari",
|
||||
"/languages/inc": "Indic (Other)",
|
||||
"/languages/frm": "French, Middle (ca. 1300-1600)",
|
||||
"/languages/esp": "Esperanto",
|
||||
"/languages/hau": "Hausa",
|
||||
"/languages/tag": "Tagalog",
|
||||
"/languages/tuk": "Turkmen",
|
||||
"/languages/enm": "English, Middle (1100-1500)",
|
||||
"/languages/map": "Austronesian (Other)",
|
||||
"/languages/pli": "Pali",
|
||||
"/languages/fro": "French, Old (ca. 842-1300)",
|
||||
"/languages/nic": "Niger-Kordofanian (Other)",
|
||||
"/languages/tir": "Tigrinya",
|
||||
"/languages/wen": "Sorbian (Other)",
|
||||
"/languages/bho": "Bhojpuri",
|
||||
"/languages/roa": "Romance (Other)",
|
||||
"/languages/tut": "Altaic (Other)",
|
||||
"/languages/bra": "Braj",
|
||||
"/languages/sun": "Sundanese",
|
||||
"/languages/fiu": "Finno-Ugrian (Other)",
|
||||
"/languages/far": "Faroese",
|
||||
"/languages/ban": "Balinese",
|
||||
"/languages/tar": "Tatar",
|
||||
"/languages/bak": "Bashkir",
|
||||
"/languages/tat": "Tatar",
|
||||
"/languages/chu": "Church Slavic",
|
||||
"/languages/dra": "Dravidian (Other)",
|
||||
"/languages/pra": "Prakrit languages",
|
||||
"/languages/paa": "Papuan (Other)",
|
||||
"/languages/doi": "Dogri",
|
||||
"/languages/lah": "Lahndā",
|
||||
"/languages/mni": "Manipuri",
|
||||
"/languages/yor": "Yoruba",
|
||||
"/languages/gmh": "German, Middle High (ca. 1050-1500)",
|
||||
"/languages/kas": "Kashmiri",
|
||||
"/languages/fri": "Frisian",
|
||||
"/languages/mla": "Malagasy",
|
||||
"/languages/egy": "Egyptian",
|
||||
"/languages/rom": "Romani",
|
||||
"/languages/syr": "Syriac, Modern",
|
||||
"/languages/cau": "Caucasian (Other)",
|
||||
"/languages/hbs": "Serbo-Croatian",
|
||||
"/languages/sai": "South American Indian (Other)",
|
||||
"/languages/pro": "Provençal (to 1500)",
|
||||
"/languages/cpf": "Creoles and Pidgins, French-based (Other)",
|
||||
"/languages/ang": "English, Old (ca. 450-1100)",
|
||||
"/languages/bal": "Baluchi",
|
||||
"/languages/gla": "Scottish Gaelic",
|
||||
"/languages/chv": "Chuvash",
|
||||
"/languages/kin": "Kinyarwanda",
|
||||
"/languages/zul": "Zulu",
|
||||
"/languages/sla": "Slavic (Other)",
|
||||
"/languages/som": "Somali",
|
||||
"/languages/mlt": "Maltese",
|
||||
"/languages/uig": "Uighur",
|
||||
"/languages/mlg": "Malagasy",
|
||||
"/languages/sho": "Shona",
|
||||
"/languages/lan": "Occitan (post 1500)",
|
||||
"/languages/bre": "Breton",
|
||||
"/languages/sco": "Scots",
|
||||
"/languages/sso": "Sotho",
|
||||
"/languages/myn": "Mayan languages",
|
||||
"/languages/xho": "Xhosa",
|
||||
"/languages/gem": "Germanic (Other)",
|
||||
"/languages/esk": "Eskimo languages",
|
||||
"/languages/akk": "Akkadian",
|
||||
"/languages/div": "Maldivian",
|
||||
"/languages/sah": "Yakut",
|
||||
"/languages/tsw": "Tswana",
|
||||
"/languages/nso": "Northern Sotho",
|
||||
"/languages/pap": "Papiamento",
|
||||
"/languages/bnt": "Bantu (Other)",
|
||||
"/languages/oss": "Ossetic",
|
||||
"/languages/cre": "Cree",
|
||||
"/languages/ibo": "Igbo",
|
||||
"/languages/fao": "Faroese",
|
||||
"/languages/nai": "North American Indian (Other)",
|
||||
"/languages/mag": "Magahi",
|
||||
"/languages/arc": "Aramaic",
|
||||
"/languages/epo": "Esperanto",
|
||||
"/languages/kha": "Khasi",
|
||||
"/languages/oji": "Ojibwa",
|
||||
"/languages/que": "Quechua",
|
||||
"/languages/lug": "Ganda",
|
||||
"/languages/mwr": "Marwari",
|
||||
"/languages/awa": "Awadhi ",
|
||||
"/languages/cor": "Cornish",
|
||||
"/languages/lad": "Ladino",
|
||||
"/languages/dzo": "Dzongkha",
|
||||
"/languages/cop": "Coptic",
|
||||
"/languages/nah": "Nahuatl",
|
||||
"/languages/cai": "Central American Indian (Other)",
|
||||
"/languages/phi": "Philippine (Other)",
|
||||
"/languages/moh": "Mohawk",
|
||||
"/languages/crp": "Creoles and Pidgins (Other)",
|
||||
"/languages/nya": "Nyanja",
|
||||
"/languages/wol": "Wolof ",
|
||||
"/languages/haw": "Hawaiian",
|
||||
"/languages/eth": "Ethiopic",
|
||||
"/languages/mis": "Miscellaneous languages",
|
||||
"/languages/mkh": "Mon-Khmer (Other)",
|
||||
"/languages/alg": "Algonquian (Other)",
|
||||
"/languages/nde": "Ndebele (Zimbabwe)",
|
||||
"/languages/ssa": "Nilo-Saharan (Other)",
|
||||
"/languages/chm": "Mari",
|
||||
"/languages/che": "Chechen",
|
||||
"/languages/gez": "Ethiopic",
|
||||
"/languages/ven": "Venda",
|
||||
"/languages/cam": "Khmer",
|
||||
"/languages/fur": "Friulian",
|
||||
"/languages/ful": "Fula",
|
||||
"/languages/gal": "Oromo",
|
||||
"/languages/jrb": "Judeo-Arabic",
|
||||
"/languages/bua": "Buriat",
|
||||
"/languages/ady": "Adygei",
|
||||
"/languages/bem": "Bemba",
|
||||
"/languages/kar": "Karen languages",
|
||||
"/languages/sna": "Shona",
|
||||
"/languages/twi": "Twi",
|
||||
"/languages/btk": "Batak",
|
||||
"/languages/kaa": "Kara-Kalpak",
|
||||
"/languages/kom": "Komi",
|
||||
"/languages/sot": "Sotho",
|
||||
"/languages/tso": "Tsonga",
|
||||
"/languages/cpe": "Creoles and Pidgins, English-based (Other)",
|
||||
"/languages/gua": "Guarani",
|
||||
"/languages/mao": "Maori",
|
||||
"/languages/mic": "Micmac",
|
||||
"/languages/swz": "Swazi",
|
||||
"/languages/taj": "Tajik",
|
||||
"/languages/smo": "Samoan",
|
||||
"/languages/ace": "Achinese",
|
||||
"/languages/afa": "Afroasiatic (Other)",
|
||||
"/languages/lap": "Sami",
|
||||
"/languages/min": "Minangkabau",
|
||||
"/languages/oci": "Occitan (post 1500)",
|
||||
"/languages/tsn": "Tswana",
|
||||
"/languages/pal": "Pahlavi",
|
||||
"/languages/sux": "Sumerian",
|
||||
"/languages/ewe": "Ewe",
|
||||
"/languages/him": "Himachali",
|
||||
"/languages/kaw": "Kawi",
|
||||
"/languages/lus": "Lushai",
|
||||
"/languages/ceb": "Cebuano",
|
||||
"/languages/chr": "Cherokee",
|
||||
"/languages/fil": "Filipino",
|
||||
"/languages/ndo": "Ndonga",
|
||||
"/languages/ilo": "Iloko",
|
||||
"/languages/kbd": "Kabardian",
|
||||
"/languages/orm": "Oromo",
|
||||
"/languages/dum": "Dutch, Middle (ca. 1050-1350)",
|
||||
"/languages/bam": "Bambara",
|
||||
"/languages/goh": "Old High German",
|
||||
"/languages/got": "Gothic",
|
||||
"/languages/kon": "Kongo",
|
||||
"/languages/mun": "Munda (Other)",
|
||||
"/languages/kru": "Kurukh",
|
||||
"/languages/pam": "Pampanga",
|
||||
"/languages/grn": "Guarani",
|
||||
"/languages/gaa": "Gã",
|
||||
"/languages/fry": "Frisian",
|
||||
"/languages/iba": "Iban",
|
||||
"/languages/mak": "Makasar",
|
||||
"/languages/kik": "Kikuyu",
|
||||
"/languages/cho": "Choctaw",
|
||||
"/languages/cpp": "Creoles and Pidgins, Portuguese-based (Other)",
|
||||
"/languages/dak": "Dakota",
|
||||
"/languages/udm": "Udmurt ",
|
||||
"/languages/hat": "Haitian French Creole",
|
||||
"/languages/mus": "Creek",
|
||||
"/languages/ber": "Berber (Other)",
|
||||
"/languages/hil": "Hiligaynon",
|
||||
"/languages/iro": "Iroquoian (Other)",
|
||||
"/languages/kua": "Kuanyama",
|
||||
"/languages/mno": "Manobo languages",
|
||||
"/languages/run": "Rundi",
|
||||
"/languages/sat": "Santali",
|
||||
"/languages/shn": "Shan",
|
||||
"/languages/tyv": "Tuvinian",
|
||||
"/languages/chg": "Chagatai",
|
||||
"/languages/syc": "Syriac",
|
||||
"/languages/ath": "Athapascan (Other)",
|
||||
"/languages/aym": "Aymara",
|
||||
"/languages/bug": "Bugis",
|
||||
"/languages/cel": "Celtic (Other)",
|
||||
"/languages/int": "Interlingua (International Auxiliary Language Association)",
|
||||
"/languages/xal": "Oirat",
|
||||
"/languages/ava": "Avaric",
|
||||
"/languages/son": "Songhai",
|
||||
"/languages/tah": "Tahitian",
|
||||
"/languages/tet": "Tetum",
|
||||
"/languages/ira": "Iranian (Other)",
|
||||
"/languages/kac": "Kachin",
|
||||
"/languages/nob": "Norwegian (Bokmål)",
|
||||
"/languages/vai": "Vai",
|
||||
"/languages/bik": "Bikol",
|
||||
"/languages/mos": "Mooré",
|
||||
"/languages/tig": "Tigré",
|
||||
"/languages/fat": "Fanti",
|
||||
"/languages/her": "Herero",
|
||||
"/languages/kal": "Kalâtdlisut",
|
||||
"/languages/mad": "Madurese",
|
||||
"/languages/yue": "Cantonese",
|
||||
"/languages/chn": "Chinook jargon",
|
||||
"/languages/hmn": "Hmong",
|
||||
"/languages/lin": "Lingala",
|
||||
"/languages/man": "Mandingo",
|
||||
"/languages/nds": "Low German",
|
||||
"/languages/bas": "Basa",
|
||||
"/languages/gay": "Gayo",
|
||||
"/languages/gsw": "gsw",
|
||||
"/languages/ine": "Indo-European (Other)",
|
||||
"/languages/kro": "Kru (Other)",
|
||||
"/languages/kum": "Kumyk",
|
||||
"/languages/tsi": "Tsimshian",
|
||||
"/languages/zap": "Zapotec",
|
||||
"/languages/ach": "Acoli",
|
||||
"/languages/ada": "Adangme",
|
||||
"/languages/aka": "Akan",
|
||||
"/languages/khi": "Khoisan (Other)",
|
||||
"/languages/srd": "Sardinian",
|
||||
"/languages/arn": "Mapuche",
|
||||
"/languages/dyu": "Dyula",
|
||||
"/languages/loz": "Lozi",
|
||||
"/languages/ltz": "Luxembourgish",
|
||||
"/languages/sag": "Sango (Ubangi Creole)",
|
||||
"/languages/lez": "Lezgian",
|
||||
"/languages/luo": "Luo (Kenya and Tanzania)",
|
||||
"/languages/ssw": "Swazi ",
|
||||
"/languages/krc": "Karachay-Balkar",
|
||||
"/languages/nyn": "Nyankole",
|
||||
"/languages/sal": "Salishan languages",
|
||||
"/languages/jpr": "Judeo-Persian",
|
||||
"/languages/pau": "Palauan",
|
||||
"/languages/smi": "Sami",
|
||||
"/languages/aar": "Afar",
|
||||
"/languages/abk": "Abkhaz",
|
||||
"/languages/gon": "Gondi",
|
||||
"/languages/nzi": "Nzima",
|
||||
"/languages/sam": "Samaritan Aramaic",
|
||||
"/languages/sao": "Samoan",
|
||||
"/languages/srr": "Serer",
|
||||
"/languages/apa": "Apache languages",
|
||||
"/languages/crh": "Crimean Tatar",
|
||||
"/languages/efi": "Efik",
|
||||
"/languages/iku": "Inuktitut",
|
||||
"/languages/nav": "Navajo",
|
||||
"/languages/pon": "Ponape",
|
||||
"/languages/tmh": "Tamashek",
|
||||
"/languages/aus": "Australian languages",
|
||||
"/languages/oto": "Otomian languages",
|
||||
"/languages/war": "Waray",
|
||||
"/languages/ypk": "Yupik languages",
|
||||
"/languages/ave": "Avestan",
|
||||
"/languages/cus": "Cushitic (Other)",
|
||||
"/languages/del": "Delaware",
|
||||
"/languages/fon": "Fon",
|
||||
"/languages/ina": "Interlingua (International Auxiliary Language Association)",
|
||||
"/languages/myv": "Erzya",
|
||||
"/languages/pag": "Pangasinan",
|
||||
"/languages/peo": "Old Persian (ca. 600-400 B.C.)",
|
||||
"/languages/vls": "Flemish",
|
||||
"/languages/bai": "Bamileke languages",
|
||||
"/languages/bla": "Siksika",
|
||||
"/languages/day": "Dayak",
|
||||
"/languages/men": "Mende",
|
||||
"/languages/tai": "Tai",
|
||||
"/languages/ton": "Tongan",
|
||||
"/languages/uga": "Ugaritic",
|
||||
"/languages/yao": "Yao (Africa)",
|
||||
"/languages/zza": "Zaza",
|
||||
"/languages/bin": "Edo",
|
||||
"/languages/frs": "East Frisian",
|
||||
"/languages/inh": "Ingush",
|
||||
"/languages/mah": "Marshallese",
|
||||
"/languages/sem": "Semitic (Other)",
|
||||
"/languages/art": "Artificial (Other)",
|
||||
"/languages/chy": "Cheyenne",
|
||||
"/languages/cmc": "Chamic languages",
|
||||
"/languages/dar": "Dargwa",
|
||||
"/languages/dua": "Duala",
|
||||
"/languages/elx": "Elamite",
|
||||
"/languages/fan": "Fang",
|
||||
"/languages/fij": "Fijian",
|
||||
"/languages/gil": "Gilbertese",
|
||||
"/languages/ijo": "Ijo",
|
||||
"/languages/kam": "Kamba",
|
||||
"/languages/nog": "Nogai",
|
||||
"/languages/non": "Old Norse",
|
||||
"/languages/tem": "Temne",
|
||||
"/languages/arg": "Aragonese",
|
||||
"/languages/arp": "Arapaho",
|
||||
"/languages/arw": "Arawak",
|
||||
"/languages/din": "Dinka",
|
||||
"/languages/grb": "Grebo",
|
||||
"/languages/kos": "Kusaie",
|
||||
"/languages/lub": "Luba-Katanga",
|
||||
"/languages/mnc": "Manchu",
|
||||
"/languages/nyo": "Nyoro",
|
||||
"/languages/rar": "Rarotongan",
|
||||
"/languages/sel": "Selkup",
|
||||
"/languages/tkl": "Tokelauan",
|
||||
"/languages/tog": "Tonga (Nyasa)",
|
||||
"/languages/tum": "Tumbuka",
|
||||
"/languages/alt": "Altai",
|
||||
"/languages/ase": "American Sign Language",
|
||||
"/languages/ast": "Asturian",
|
||||
"/languages/chk": "Chuukese",
|
||||
"/languages/cos": "Corsican",
|
||||
"/languages/ewo": "Ewondo",
|
||||
"/languages/gor": "Gorontalo",
|
||||
"/languages/hmo": "Hiri Motu",
|
||||
"/languages/lol": "Mongo-Nkundu",
|
||||
"/languages/lun": "Lunda",
|
||||
"/languages/mas": "Masai",
|
||||
"/languages/niu": "Niuean",
|
||||
"/languages/rup": "Aromanian",
|
||||
"/languages/sas": "Sasak",
|
||||
"/languages/sio": "Siouan (Other)",
|
||||
"/languages/sus": "Susu",
|
||||
"/languages/zun": "Zuni",
|
||||
"/languages/bat": "Baltic (Other)",
|
||||
"/languages/car": "Carib",
|
||||
"/languages/cha": "Chamorro",
|
||||
"/languages/kab": "Kabyle",
|
||||
"/languages/kau": "Kanuri",
|
||||
"/languages/kho": "Khotanese",
|
||||
"/languages/lua": "Luba-Lulua",
|
||||
"/languages/mdf": "Moksha",
|
||||
"/languages/nbl": "Ndebele (South Africa)",
|
||||
"/languages/umb": "Umbundu",
|
||||
"/languages/wak": "Wakashan languages",
|
||||
"/languages/wal": "Wolayta",
|
||||
"/languages/ale": "Aleut",
|
||||
"/languages/bis": "Bislama",
|
||||
"/languages/gba": "Gbaya",
|
||||
"/languages/glv": "Manx",
|
||||
"/languages/gul": "Gullah",
|
||||
"/languages/ipk": "Inupiaq",
|
||||
"/languages/krl": "Karelian",
|
||||
"/languages/lam": "Lamba (Zambia and Congo)",
|
||||
"/languages/sad": "Sandawe",
|
||||
"/languages/sid": "Sidamo",
|
||||
"/languages/snk": "Soninke",
|
||||
"/languages/srn": "Sranan",
|
||||
"/languages/suk": "Sukuma",
|
||||
"/languages/ter": "Terena",
|
||||
"/languages/tiv": "Tiv",
|
||||
"/languages/tli": "Tlingit",
|
||||
"/languages/tpi": "Tok Pisin",
|
||||
"/languages/tvl": "Tuvaluan",
|
||||
"/languages/yap": "Yapese",
|
||||
"/languages/eka": "Ekajuk",
|
||||
"/languages/hsb": "Upper Sorbian",
|
||||
"/languages/ido": "Ido",
|
||||
"/languages/kmb": "Kimbundu",
|
||||
"/languages/kpe": "Kpelle",
|
||||
"/languages/mwl": "Mirandese",
|
||||
"/languages/nno": "Nynorsk",
|
||||
"/languages/nub": "Nubian languages",
|
||||
"/languages/osa": "Osage",
|
||||
"/languages/sme": "Northern Sami",
|
||||
"/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
|
||||
import operator
|
||||
|
||||
|
@ -10,10 +10,11 @@ from .abstract_connector import AbstractConnector, SearchResult
|
|||
|
||||
|
||||
class Connector(AbstractConnector):
|
||||
''' instantiate a connector '''
|
||||
""" instantiate a connector """
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def search(self, query, min_confidence=0.1, raw=False):
|
||||
''' search your local database '''
|
||||
""" search your local database """
|
||||
if not query:
|
||||
return []
|
||||
# first, try searching unqiue identifiers
|
||||
|
@ -33,19 +34,45 @@ class Connector(AbstractConnector):
|
|||
search_results.sort(key=lambda r: r.confidence, reverse=True)
|
||||
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):
|
||||
return SearchResult(
|
||||
title=search_result.title,
|
||||
key=search_result.remote_id,
|
||||
author=search_result.author_text,
|
||||
year=search_result.published_date.year if \
|
||||
search_result.published_date else None,
|
||||
year=search_result.published_date.year
|
||||
if search_result.published_date
|
||||
else None,
|
||||
connector=self,
|
||||
confidence=search_result.rank if \
|
||||
hasattr(search_result, 'rank') else 1,
|
||||
cover="%s%s" % (self.covers_url, search_result.cover),
|
||||
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):
|
||||
pass
|
||||
|
@ -59,8 +86,12 @@ class Connector(AbstractConnector):
|
|||
def get_authors_from_data(self, data):
|
||||
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):
|
||||
''' 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
|
||||
|
||||
def expand_book_data(self, book):
|
||||
|
@ -68,44 +99,47 @@ class Connector(AbstractConnector):
|
|||
|
||||
|
||||
def search_identifiers(query):
|
||||
''' tries remote_id, isbn; defined as dedupe fields on the model '''
|
||||
filters = [{f.name: query} for f in models.Edition._meta.get_fields() \
|
||||
if hasattr(f, 'deduplication_field') and f.deduplication_field]
|
||||
""" tries remote_id, isbn; defined as dedupe fields on the model """
|
||||
filters = [
|
||||
{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(
|
||||
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.
|
||||
return results.filter(parent_work__default_edition__id=F('id')) \
|
||||
or results
|
||||
return results.filter(parent_work__default_edition__id=F("id")) or results
|
||||
|
||||
|
||||
def search_title_author(query, min_confidence):
|
||||
''' searches for title and author '''
|
||||
vector = SearchVector('title', weight='A') +\
|
||||
SearchVector('subtitle', weight='B') +\
|
||||
SearchVector('authors__name', weight='C') +\
|
||||
SearchVector('series', weight='D')
|
||||
""" searches for title and author """
|
||||
vector = (
|
||||
SearchVector("title", weight="A")
|
||||
+ SearchVector("subtitle", weight="B")
|
||||
+ SearchVector("authors__name", weight="C")
|
||||
+ SearchVector("series", weight="D")
|
||||
)
|
||||
|
||||
results = models.Edition.objects.annotate(
|
||||
search=vector
|
||||
).annotate(
|
||||
rank=SearchRank(vector, query)
|
||||
).filter(
|
||||
rank__gt=min_confidence
|
||||
).order_by('-rank')
|
||||
results = (
|
||||
models.Edition.objects.annotate(search=vector)
|
||||
.annotate(rank=SearchRank(vector, query))
|
||||
.filter(rank__gt=min_confidence)
|
||||
.order_by("-rank")
|
||||
)
|
||||
|
||||
# when there are multiple editions of the same work, pick the closest
|
||||
editions_of_work = results.values(
|
||||
'parent_work'
|
||||
).annotate(
|
||||
Count('parent_work')
|
||||
).values_list('parent_work')
|
||||
editions_of_work = (
|
||||
results.values("parent_work")
|
||||
.annotate(Count("parent_work"))
|
||||
.values_list("parent_work")
|
||||
)
|
||||
|
||||
for work_id in set(editions_of_work):
|
||||
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
|
||||
# if mutliple books have the top rank, pick the default edition
|
||||
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
|
||||
|
||||
def site_settings(request):# pylint: disable=unused-argument
|
||||
''' include the custom info about the site '''
|
||||
return {
|
||||
'site': models.SiteSettings.objects.get()
|
||||
}
|
||||
|
||||
def site_settings(request): # pylint: disable=unused-argument
|
||||
""" include the custom info about the site """
|
||||
return {"site": models.SiteSettings.objects.get()}
|
||||
|
|
|
@ -1,25 +1,66 @@
|
|||
''' send emails '''
|
||||
from django.core.mail import send_mail
|
||||
""" send emails """
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import get_template
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm import models, settings
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
def email_data():
|
||||
""" fields every email needs """
|
||||
site = models.SiteSettings.objects.get()
|
||||
if site.logo_small:
|
||||
logo_path = "/images/{}".format(site.logo_small.url)
|
||||
else:
|
||||
logo_path = "/static/images/logo-small.png"
|
||||
|
||||
return {
|
||||
"site_name": site.name,
|
||||
"logo": logo_path,
|
||||
"domain": DOMAIN,
|
||||
"user": None,
|
||||
}
|
||||
|
||||
|
||||
def invite_email(invite_request):
|
||||
""" send out an invite code """
|
||||
data = email_data()
|
||||
data["invite_link"] = invite_request.invite.link
|
||||
send_email.delay(invite_request.email, *format_email("invite", data))
|
||||
|
||||
|
||||
def password_reset_email(reset_code):
|
||||
''' generate a password reset email '''
|
||||
site = models.SiteSettings.get()
|
||||
send_email.delay(
|
||||
reset_code.user.email,
|
||||
'Reset your password on %s' % site.name,
|
||||
'Your password reset link: %s' % reset_code.link
|
||||
""" generate a password reset email """
|
||||
data = email_data()
|
||||
data["reset_link"] = reset_code.link
|
||||
data["user"] = reset_code.user.display_name
|
||||
send_email.delay(reset_code.user.email, *format_email("password_reset", data))
|
||||
|
||||
|
||||
def format_email(email_name, data):
|
||||
""" render the email templates """
|
||||
subject = (
|
||||
get_template("email/{}/subject.html".format(email_name)).render(data).strip()
|
||||
)
|
||||
html_content = (
|
||||
get_template("email/{}/html_content.html".format(email_name))
|
||||
.render(data)
|
||||
.strip()
|
||||
)
|
||||
text_content = (
|
||||
get_template("email/{}/text_content.html".format(email_name))
|
||||
.render(data)
|
||||
.strip()
|
||||
)
|
||||
return (subject, html_content, text_content)
|
||||
|
||||
|
||||
@app.task
|
||||
def send_email(recipient, subject, message):
|
||||
''' use a task to send the email '''
|
||||
send_mail(
|
||||
subject,
|
||||
message,
|
||||
None, # sender will be the config default
|
||||
[recipient],
|
||||
fail_silently=False
|
||||
def send_email(recipient, subject, html_content, text_content):
|
||||
""" use a task to send the email """
|
||||
email = EmailMultiAlternatives(
|
||||
subject, text_content, settings.DEFAULT_FROM_EMAIL, [recipient]
|
||||
)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
email.send()
|
||||
|
|
|
@ -1,103 +1,124 @@
|
|||
''' using django model forms '''
|
||||
""" using django model forms """
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from django import forms
|
||||
from django.forms import ModelForm, PasswordInput, widgets
|
||||
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
|
||||
from django.forms.widgets import Textarea
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
class CustomForm(ModelForm):
|
||||
''' add css classes to the forms '''
|
||||
""" add css classes to the forms """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
css_classes = defaultdict(lambda: '')
|
||||
css_classes['text'] = 'input'
|
||||
css_classes['password'] = 'input'
|
||||
css_classes['email'] = 'input'
|
||||
css_classes['number'] = 'input'
|
||||
css_classes['checkbox'] = 'checkbox'
|
||||
css_classes['textarea'] = 'textarea'
|
||||
css_classes = defaultdict(lambda: "")
|
||||
css_classes["text"] = "input"
|
||||
css_classes["password"] = "input"
|
||||
css_classes["email"] = "input"
|
||||
css_classes["number"] = "input"
|
||||
css_classes["checkbox"] = "checkbox"
|
||||
css_classes["textarea"] = "textarea"
|
||||
super(CustomForm, self).__init__(*args, **kwargs)
|
||||
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
|
||||
if isinstance(visible.field.widget, Textarea):
|
||||
input_type = 'textarea'
|
||||
visible.field.widget.attrs['cols'] = None
|
||||
visible.field.widget.attrs['rows'] = None
|
||||
visible.field.widget.attrs['class'] = css_classes[input_type]
|
||||
input_type = "textarea"
|
||||
visible.field.widget.attrs["cols"] = None
|
||||
visible.field.widget.attrs["rows"] = None
|
||||
visible.field.widget.attrs["class"] = css_classes[input_type]
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class LoginForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ['localname', 'password']
|
||||
fields = ["localname", "password"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
'password': PasswordInput(),
|
||||
"password": PasswordInput(),
|
||||
}
|
||||
|
||||
|
||||
class RegisterForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ['localname', 'email', 'password']
|
||||
fields = ["localname", "email", "password"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
'password': PasswordInput()
|
||||
}
|
||||
widgets = {"password": PasswordInput()}
|
||||
|
||||
|
||||
class RatingForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Review
|
||||
fields = ['user', 'book', 'content', 'rating', 'privacy']
|
||||
model = models.ReviewRating
|
||||
fields = ["user", "book", "rating", "privacy"]
|
||||
|
||||
|
||||
class ReviewForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Review
|
||||
fields = [
|
||||
'user', 'book',
|
||||
'name', 'content', 'rating',
|
||||
'content_warning', 'sensitive',
|
||||
'privacy']
|
||||
"user",
|
||||
"book",
|
||||
"name",
|
||||
"content",
|
||||
"rating",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
]
|
||||
|
||||
|
||||
class CommentForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Comment
|
||||
fields = [
|
||||
'user', 'book', 'content',
|
||||
'content_warning', 'sensitive',
|
||||
'privacy']
|
||||
"user",
|
||||
"book",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
"progress",
|
||||
"progress_mode",
|
||||
]
|
||||
|
||||
|
||||
class QuotationForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Quotation
|
||||
fields = [
|
||||
'user', 'book', 'quote', 'content',
|
||||
'content_warning', 'sensitive', 'privacy']
|
||||
"user",
|
||||
"book",
|
||||
"quote",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
]
|
||||
|
||||
|
||||
class ReplyForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = [
|
||||
'user', 'content', 'content_warning', 'sensitive',
|
||||
'reply_parent', 'privacy']
|
||||
"user",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"reply_parent",
|
||||
"privacy",
|
||||
]
|
||||
|
||||
|
||||
class StatusForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = [
|
||||
'user', 'content', 'content_warning', 'sensitive', 'privacy']
|
||||
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||
|
||||
class ListStatusForm(CustomForm):
|
||||
class Meta:
|
||||
|
@ -110,23 +131,49 @@ class EditUserForm(CustomForm):
|
|||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
'avatar', 'name', 'email', 'summary', 'manually_approves_followers'
|
||||
"avatar",
|
||||
"name",
|
||||
"email",
|
||||
"summary",
|
||||
"show_goal",
|
||||
"manually_approves_followers",
|
||||
"discoverable",
|
||||
"preferred_timezone",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
|
||||
|
||||
class LimitedEditUserForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
"avatar",
|
||||
"name",
|
||||
"summary",
|
||||
"manually_approves_followers",
|
||||
"discoverable",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
|
||||
|
||||
class UserGroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["groups"]
|
||||
|
||||
|
||||
class TagForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Tag
|
||||
fields = ['name']
|
||||
fields = ["name"]
|
||||
help_texts = {f: None for f in fields}
|
||||
labels = {'name': 'Add a tag'}
|
||||
labels = {"name": "Add a tag"}
|
||||
|
||||
|
||||
class CoverForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Book
|
||||
fields = ['cover']
|
||||
fields = ["cover"]
|
||||
help_texts = {f: None for f in fields}
|
||||
|
||||
|
||||
|
@ -134,80 +181,100 @@ class EditionForm(CustomForm):
|
|||
class Meta:
|
||||
model = models.Edition
|
||||
exclude = [
|
||||
'remote_id',
|
||||
'origin_id',
|
||||
'created_date',
|
||||
'updated_date',
|
||||
'edition_rank',
|
||||
|
||||
'authors',# TODO
|
||||
'parent_work',
|
||||
'shelves',
|
||||
|
||||
'subjects',# TODO
|
||||
'subject_places',# TODO
|
||||
|
||||
'connector',
|
||||
"remote_id",
|
||||
"origin_id",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"edition_rank",
|
||||
"authors",
|
||||
"parent_work",
|
||||
"shelves",
|
||||
"subjects", # TODO
|
||||
"subject_places", # TODO
|
||||
"connector",
|
||||
]
|
||||
|
||||
|
||||
class AuthorForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Author
|
||||
exclude = [
|
||||
'remote_id',
|
||||
'origin_id',
|
||||
'created_date',
|
||||
'updated_date',
|
||||
"remote_id",
|
||||
"origin_id",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
]
|
||||
|
||||
|
||||
class ImportForm(forms.Form):
|
||||
csv_file = forms.FileField()
|
||||
|
||||
|
||||
class ExpiryWidget(widgets.Select):
|
||||
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)
|
||||
|
||||
if selected_string == 'day':
|
||||
if selected_string == "day":
|
||||
interval = datetime.timedelta(days=1)
|
||||
elif selected_string == 'week':
|
||||
elif selected_string == "week":
|
||||
interval = datetime.timedelta(days=7)
|
||||
elif selected_string == 'month':
|
||||
interval = datetime.timedelta(days=31) # Close enough?
|
||||
elif selected_string == 'forever':
|
||||
elif selected_string == "month":
|
||||
interval = datetime.timedelta(days=31) # Close enough?
|
||||
elif selected_string == "forever":
|
||||
return None
|
||||
else:
|
||||
return selected_string # "This will raise
|
||||
return selected_string # "This will raise
|
||||
|
||||
return timezone.now() + interval
|
||||
|
||||
|
||||
class InviteRequestForm(CustomForm):
|
||||
def clean(self):
|
||||
""" make sure the email isn't in use by a registered user """
|
||||
cleaned_data = super().clean()
|
||||
email = cleaned_data.get("email")
|
||||
if email and models.User.objects.filter(email=email).exists():
|
||||
self.add_error("email", _("A user with this email already exists."))
|
||||
|
||||
class Meta:
|
||||
model = models.InviteRequest
|
||||
fields = ["email"]
|
||||
|
||||
|
||||
class CreateInviteForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.SiteInvite
|
||||
exclude = ['code', 'user', 'times_used']
|
||||
exclude = ["code", "user", "times_used", "invitees"]
|
||||
widgets = {
|
||||
'expiry': ExpiryWidget(choices=[
|
||||
('day', _('One Day')),
|
||||
('week', _('One Week')),
|
||||
('month', _('One Month')),
|
||||
('forever', _('Does Not Expire'))]),
|
||||
'use_limit': widgets.Select(
|
||||
choices=[(i, _("%(count)d uses" % {'count': i})) \
|
||||
for i in [1, 5, 10, 25, 50, 100]]
|
||||
+ [(None, _('Unlimited'))])
|
||||
"expiry": ExpiryWidget(
|
||||
choices=[
|
||||
("day", _("One Day")),
|
||||
("week", _("One Week")),
|
||||
("month", _("One Month")),
|
||||
("forever", _("Does Not Expire")),
|
||||
]
|
||||
),
|
||||
"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 Meta:
|
||||
model = models.Shelf
|
||||
fields = ['user', 'name', 'privacy']
|
||||
fields = ["user", "name", "privacy"]
|
||||
|
||||
|
||||
class GoalForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.AnnualGoal
|
||||
fields = ['user', 'year', 'goal', 'privacy']
|
||||
fields = ["user", "year", "goal", "privacy"]
|
||||
|
||||
|
||||
class SiteForm(CustomForm):
|
||||
|
@ -219,4 +286,33 @@ class SiteForm(CustomForm):
|
|||
class ListForm(CustomForm):
|
||||
class Meta:
|
||||
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"]
|
||||
|
||||
|
||||
class ServerForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.FederatedServer
|
||||
exclude = ["remote_id"]
|
||||
|
||||
|
||||
class SortListForm(forms.Form):
|
||||
sort_by = ChoiceField(
|
||||
choices=(
|
||||
("order", _("List Order")),
|
||||
("title", _("Book Title")),
|
||||
("rating", _("Rating")),
|
||||
),
|
||||
label=_("Sort By"),
|
||||
)
|
||||
direction = ChoiceField(
|
||||
choices=(
|
||||
("ascending", _("Ascending")),
|
||||
("descending", _("Descending")),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
''' handle reading a csv from goodreads '''
|
||||
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
|
||||
|
||||
class GoodreadsImporter(Importer):
|
||||
service = 'GoodReads'
|
||||
|
||||
def parse_fields(self, data):
|
||||
data.update({'import_source': self.service })
|
||||
# add missing 'Date Started' field
|
||||
data.update({'Date Started': None })
|
||||
return data
|
5
bookwyrm/importers/__init__.py
Normal file
5
bookwyrm/importers/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
""" import classes """
|
||||
|
||||
from .importer import Importer
|
||||
from .goodreads_import import GoodreadsImporter
|
||||
from .librarything_import import LibrarythingImporter
|
16
bookwyrm/importers/goodreads_import.py
Normal file
16
bookwyrm/importers/goodreads_import.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
""" handle reading a csv from goodreads """
|
||||
from . import Importer
|
||||
|
||||
|
||||
class GoodreadsImporter(Importer):
|
||||
"""GoodReads is the default importer, thus Importer follows its structure.
|
||||
For a more complete example of overriding see librarything_import.py"""
|
||||
|
||||
service = "GoodReads"
|
||||
|
||||
def parse_fields(self, entry):
|
||||
""" handle the specific fields in goodreads csvs """
|
||||
entry.update({"import_source": self.service})
|
||||
# add missing 'Date Started' field
|
||||
entry.update({"Date Started": None})
|
||||
return entry
|
|
@ -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 logging
|
||||
|
||||
|
@ -8,49 +8,52 @@ from bookwyrm.tasks import app
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Importer:
|
||||
service = 'Unknown'
|
||||
delimiter = ','
|
||||
encoding = 'UTF-8'
|
||||
mandatory_fields = ['Title', 'Author']
|
||||
""" Generic class for csv data import from an outside service """
|
||||
|
||||
service = "Unknown"
|
||||
delimiter = ","
|
||||
encoding = "UTF-8"
|
||||
mandatory_fields = ["Title", "Author"]
|
||||
|
||||
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(
|
||||
user=user,
|
||||
include_reviews=include_reviews,
|
||||
privacy=privacy
|
||||
user=user, 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):
|
||||
raise ValueError('Author and title must be in data.')
|
||||
raise ValueError("Author and title must be in data.")
|
||||
entry = self.parse_fields(entry)
|
||||
self.save_item(job, index, entry)
|
||||
return job
|
||||
|
||||
|
||||
def save_item(self, job, index, data):
|
||||
def save_item(self, job, index, data): # pylint: disable=no-self-use
|
||||
""" creates and saves an import item """
|
||||
ImportItem(job=job, index=index, data=data).save()
|
||||
|
||||
def parse_fields(self, entry):
|
||||
entry.update({'import_source': self.service })
|
||||
return entry
|
||||
""" updates csv data with additional info """
|
||||
entry.update({"import_source": self.service})
|
||||
return entry
|
||||
|
||||
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(
|
||||
user=user,
|
||||
include_reviews=original_job.include_reviews,
|
||||
privacy=original_job.privacy,
|
||||
retry=True
|
||||
retry=True,
|
||||
)
|
||||
for item in items:
|
||||
self.save_item(job, item.index, item.data)
|
||||
return job
|
||||
|
||||
|
||||
def start_import(self, job):
|
||||
''' initalizes a csv import job '''
|
||||
""" initalizes a csv import job """
|
||||
result = import_data.delay(self.service, job.id)
|
||||
job.task_id = result.id
|
||||
job.save()
|
||||
|
@ -58,15 +61,15 @@ class Importer:
|
|||
|
||||
@app.task
|
||||
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)
|
||||
try:
|
||||
for item in job.items.all():
|
||||
try:
|
||||
item.resolve()
|
||||
except Exception as e:# pylint: disable=broad-except
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.exception(e)
|
||||
item.fail_reason = 'Error loading book'
|
||||
item.fail_reason = "Error loading book"
|
||||
item.save()
|
||||
continue
|
||||
|
||||
|
@ -74,10 +77,11 @@ def import_data(source, job_id):
|
|||
item.save()
|
||||
|
||||
# shelves book and handles reviews
|
||||
handle_imported_book(source,
|
||||
job.user, item, job.include_reviews, job.privacy)
|
||||
handle_imported_book(
|
||||
source, job.user, item, job.include_reviews, job.privacy
|
||||
)
|
||||
else:
|
||||
item.fail_reason = 'Could not find a match for book'
|
||||
item.fail_reason = "Could not find a match for book"
|
||||
item.save()
|
||||
finally:
|
||||
job.complete = True
|
||||
|
@ -85,41 +89,41 @@ def import_data(source, job_id):
|
|||
|
||||
|
||||
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):
|
||||
item.book = item.book.default_edition
|
||||
if not item.book:
|
||||
return
|
||||
|
||||
existing_shelf = models.ShelfBook.objects.filter(
|
||||
book=item.book, user=user).exists()
|
||||
existing_shelf = models.ShelfBook.objects.filter(book=item.book, user=user).exists()
|
||||
|
||||
# shelve the book if it hasn't been shelved already
|
||||
if item.shelf and not existing_shelf:
|
||||
desired_shelf = models.Shelf.objects.get(
|
||||
identifier=item.shelf,
|
||||
user=user
|
||||
)
|
||||
models.ShelfBook.objects.create(
|
||||
book=item.book, shelf=desired_shelf, user=user)
|
||||
desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user)
|
||||
models.ShelfBook.objects.create(book=item.book, shelf=desired_shelf, user=user)
|
||||
|
||||
for read in item.reads:
|
||||
# check for an existing readthrough with the same dates
|
||||
if models.ReadThrough.objects.filter(
|
||||
user=user, book=item.book,
|
||||
start_date=read.start_date,
|
||||
finish_date=read.finish_date
|
||||
).exists():
|
||||
user=user,
|
||||
book=item.book,
|
||||
start_date=read.start_date,
|
||||
finish_date=read.finish_date,
|
||||
).exists():
|
||||
continue
|
||||
read.book = item.book
|
||||
read.user = user
|
||||
read.save()
|
||||
|
||||
if include_reviews and (item.rating or item.review):
|
||||
review_title = 'Review of {!r} on {!r}'.format(
|
||||
item.book.title,
|
||||
source,
|
||||
) if item.review else ''
|
||||
review_title = (
|
||||
"Review of {!r} on {!r}".format(
|
||||
item.book.title,
|
||||
source,
|
||||
)
|
||||
if item.review
|
||||
else ""
|
||||
)
|
||||
|
||||
# we don't know the publication date of the review,
|
||||
# but "now" is a bad guess
|
42
bookwyrm/importers/librarything_import.py
Normal file
42
bookwyrm/importers/librarything_import.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
""" handle reading a csv from librarything """
|
||||
import re
|
||||
import math
|
||||
|
||||
from . import Importer
|
||||
|
||||
|
||||
class LibrarythingImporter(Importer):
|
||||
""" csv downloads from librarything """
|
||||
|
||||
service = "LibraryThing"
|
||||
delimiter = "\t"
|
||||
encoding = "ISO-8859-1"
|
||||
# mandatory_fields : fields matching the book title and author
|
||||
mandatory_fields = ["Title", "Primary Author"]
|
||||
|
||||
def parse_fields(self, entry):
|
||||
""" custom parsing for librarything """
|
||||
data = {}
|
||||
data["import_source"] = self.service
|
||||
data["Book Id"] = entry["Book Id"]
|
||||
data["Title"] = entry["Title"]
|
||||
data["Author"] = entry["Primary Author"]
|
||||
data["ISBN13"] = entry["ISBN"]
|
||||
data["My Review"] = entry["Review"]
|
||||
if entry["Rating"]:
|
||||
data["My Rating"] = math.ceil(float(entry["Rating"]))
|
||||
else:
|
||||
data["My Rating"] = ""
|
||||
data["Date Added"] = re.sub(r"\[|\]", "", entry["Entry Date"])
|
||||
data["Date Started"] = re.sub(r"\[|\]", "", entry["Date Started"])
|
||||
data["Date Read"] = re.sub(r"\[|\]", "", entry["Date Read"])
|
||||
|
||||
data["Exclusive Shelf"] = None
|
||||
if data["Date Read"]:
|
||||
data["Exclusive Shelf"] = "read"
|
||||
elif data["Date Started"]:
|
||||
data["Exclusive Shelf"] = "reading"
|
||||
else:
|
||||
data["Exclusive Shelf"] = "to-read"
|
||||
|
||||
return data
|
|
@ -1,42 +0,0 @@
|
|||
''' handle reading a csv from librarything '''
|
||||
import csv
|
||||
import re
|
||||
import math
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models import ImportItem
|
||||
from bookwyrm.importer import Importer
|
||||
|
||||
|
||||
class LibrarythingImporter(Importer):
|
||||
service = 'LibraryThing'
|
||||
delimiter = '\t'
|
||||
encoding = 'ISO-8859-1'
|
||||
# mandatory_fields : fields matching the book title and author
|
||||
mandatory_fields = ['Title', 'Primary Author']
|
||||
|
||||
def parse_fields(self, initial):
|
||||
data = {}
|
||||
data['import_source'] = self.service
|
||||
data['Book Id'] = initial['Book Id']
|
||||
data['Title'] = initial['Title']
|
||||
data['Author'] = initial['Primary Author']
|
||||
data['ISBN13'] = initial['ISBN']
|
||||
data['My Review'] = initial['Review']
|
||||
if initial['Rating']:
|
||||
data['My Rating'] = math.ceil(float(initial['Rating']))
|
||||
else:
|
||||
data['My Rating'] = ''
|
||||
data['Date Added'] = re.sub('\[|\]', '', initial['Entry Date'])
|
||||
data['Date Started'] = re.sub('\[|\]', '', initial['Date Started'])
|
||||
data['Date Read'] = re.sub('\[|\]', '', initial['Date Read'])
|
||||
|
||||
data['Exclusive Shelf'] = None
|
||||
if data['Date Read']:
|
||||
data['Exclusive Shelf'] = "read"
|
||||
elif data['Date Started']:
|
||||
data['Exclusive Shelf'] = "reading"
|
||||
else:
|
||||
data['Exclusive Shelf'] = "to-read"
|
||||
|
||||
return data
|
|
@ -1,26 +1,20 @@
|
|||
''' PROCEED WITH CAUTION: uses deduplication fields to permanently
|
||||
merge book data objects '''
|
||||
""" PROCEED WITH CAUTION: uses deduplication fields to permanently
|
||||
merge book data objects """
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Count
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
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
|
||||
related_models = [
|
||||
(r.remote_field.name, r.related_model) for r in \
|
||||
canonical._meta.related_objects]
|
||||
(r.remote_field.name, r.related_model) for r in canonical._meta.related_objects
|
||||
]
|
||||
for (related_field, related_model) in related_models:
|
||||
related_objs = related_model.objects.filter(
|
||||
**{related_field: obj})
|
||||
related_objs = related_model.objects.filter(**{related_field: obj})
|
||||
for related_obj in related_objs:
|
||||
print(
|
||||
'replacing in',
|
||||
related_model.__name__,
|
||||
related_field,
|
||||
related_obj.id
|
||||
)
|
||||
print("replacing in", related_model.__name__, related_field, related_obj.id)
|
||||
try:
|
||||
setattr(related_obj, related_field, canonical)
|
||||
related_obj.save()
|
||||
|
@ -30,40 +24,41 @@ def update_related(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():
|
||||
if not hasattr(data_field, 'activitypub_field'):
|
||||
if not hasattr(data_field, "activitypub_field"):
|
||||
continue
|
||||
data_value = getattr(obj, data_field.name)
|
||||
if not data_value:
|
||||
continue
|
||||
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)
|
||||
canonical.save()
|
||||
|
||||
|
||||
def dedupe_model(model):
|
||||
''' combine duplicate editions and update related models '''
|
||||
""" combine duplicate editions and update related models """
|
||||
fields = model._meta.get_fields()
|
||||
dedupe_fields = [f for f in fields if \
|
||||
hasattr(f, 'deduplication_field') and f.deduplication_field]
|
||||
dedupe_fields = [
|
||||
f for f in fields if hasattr(f, "deduplication_field") and f.deduplication_field
|
||||
]
|
||||
for field in dedupe_fields:
|
||||
dupes = model.objects.values(field.name).annotate(
|
||||
Count(field.name)
|
||||
).filter(**{'%s__count__gt' % field.name: 1})
|
||||
dupes = (
|
||||
model.objects.values(field.name)
|
||||
.annotate(Count(field.name))
|
||||
.filter(**{"%s__count__gt" % field.name: 1})
|
||||
)
|
||||
|
||||
for dupe in dupes:
|
||||
value = dupe[field.name]
|
||||
if not value or value == '':
|
||||
if not value or value == "":
|
||||
continue
|
||||
print('----------')
|
||||
print("----------")
|
||||
print(dupe)
|
||||
objs = model.objects.filter(
|
||||
**{field.name: value}
|
||||
).order_by('id')
|
||||
objs = model.objects.filter(**{field.name: value}).order_by("id")
|
||||
canonical = objs.first()
|
||||
print('keeping', canonical.remote_id)
|
||||
print("keeping", canonical.remote_id)
|
||||
for obj in objs[1:]:
|
||||
print(obj.remote_id)
|
||||
copy_data(canonical, obj)
|
||||
|
@ -73,11 +68,12 @@ def dedupe_model(model):
|
|||
|
||||
|
||||
class Command(BaseCommand):
|
||||
''' dedplucate allllll the book data models '''
|
||||
help = 'merges duplicate book data'
|
||||
""" dedplucate allllll the book data models """
|
||||
|
||||
help = "merges duplicate book data"
|
||||
# pylint: disable=no-self-use,unused-argument
|
||||
def handle(self, *args, **options):
|
||||
''' run deudplications '''
|
||||
""" run deudplications """
|
||||
dedupe_model(models.Edition)
|
||||
dedupe_model(models.Work)
|
||||
dedupe_model(models.Author)
|
||||
|
|
24
bookwyrm/management/commands/erase_streams.py
Normal file
24
bookwyrm/management/commands/erase_streams.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
""" Delete user streams """
|
||||
from django.core.management.base import BaseCommand
|
||||
import redis
|
||||
|
||||
from bookwyrm import settings
|
||||
|
||||
r = redis.Redis(
|
||||
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0
|
||||
)
|
||||
|
||||
|
||||
def erase_streams():
|
||||
""" throw the whole redis away """
|
||||
r.flushall()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
""" delete activity streams for all users """
|
||||
|
||||
help = "Delete all the user streams"
|
||||
# pylint: disable=no-self-use,unused-argument
|
||||
def handle(self, *args, **options):
|
||||
""" flush all, baby """
|
||||
erase_streams()
|
|
@ -2,54 +2,66 @@ from django.core.management.base import BaseCommand, CommandError
|
|||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from bookwyrm.models import Connector, SiteSettings, User
|
||||
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
def init_groups():
|
||||
groups = ['admin', 'moderator', 'editor']
|
||||
groups = ["admin", "moderator", "editor"]
|
||||
for group in groups:
|
||||
Group.objects.create(name=group)
|
||||
|
||||
|
||||
def init_permissions():
|
||||
permissions = [{
|
||||
'codename': 'edit_instance_settings',
|
||||
'name': 'change the instance info',
|
||||
'groups': ['admin',]
|
||||
}, {
|
||||
'codename': 'set_user_group',
|
||||
'name': 'change what group a user is in',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'control_federation',
|
||||
'name': 'control who to federate with',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'create_invites',
|
||||
'name': 'issue invitations to join',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'moderate_user',
|
||||
'name': 'deactivate or silence a user',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'moderate_post',
|
||||
'name': 'delete other users\' posts',
|
||||
'groups': ['admin', 'moderator']
|
||||
}, {
|
||||
'codename': 'edit_book',
|
||||
'name': 'edit book info',
|
||||
'groups': ['admin', 'moderator', 'editor']
|
||||
}]
|
||||
permissions = [
|
||||
{
|
||||
"codename": "edit_instance_settings",
|
||||
"name": "change the instance info",
|
||||
"groups": [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
{
|
||||
"codename": "set_user_group",
|
||||
"name": "change what group a user is in",
|
||||
"groups": ["admin", "moderator"],
|
||||
},
|
||||
{
|
||||
"codename": "control_federation",
|
||||
"name": "control who to federate with",
|
||||
"groups": ["admin", "moderator"],
|
||||
},
|
||||
{
|
||||
"codename": "create_invites",
|
||||
"name": "issue invitations to join",
|
||||
"groups": ["admin", "moderator"],
|
||||
},
|
||||
{
|
||||
"codename": "moderate_user",
|
||||
"name": "deactivate or silence a user",
|
||||
"groups": ["admin", "moderator"],
|
||||
},
|
||||
{
|
||||
"codename": "moderate_post",
|
||||
"name": "delete other users' posts",
|
||||
"groups": ["admin", "moderator"],
|
||||
},
|
||||
{
|
||||
"codename": "edit_book",
|
||||
"name": "edit book info",
|
||||
"groups": ["admin", "moderator", "editor"],
|
||||
},
|
||||
]
|
||||
|
||||
content_type = ContentType.objects.get_for_model(User)
|
||||
for permission in permissions:
|
||||
permission_obj = Permission.objects.create(
|
||||
codename=permission['codename'],
|
||||
name=permission['name'],
|
||||
codename=permission["codename"],
|
||||
name=permission["name"],
|
||||
content_type=content_type,
|
||||
)
|
||||
# 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)
|
||||
|
||||
# while the groups and permissions shouldn't be changed because the code
|
||||
|
@ -59,46 +71,62 @@ def init_permissions():
|
|||
def init_connectors():
|
||||
Connector.objects.create(
|
||||
identifier=DOMAIN,
|
||||
name='Local',
|
||||
name="Local",
|
||||
local=True,
|
||||
connector_file='self_connector',
|
||||
base_url='https://%s' % DOMAIN,
|
||||
books_url='https://%s/book' % DOMAIN,
|
||||
covers_url='https://%s/images/covers' % DOMAIN,
|
||||
search_url='https://%s/search?q=' % DOMAIN,
|
||||
connector_file="self_connector",
|
||||
base_url="https://%s" % DOMAIN,
|
||||
books_url="https://%s/book" % DOMAIN,
|
||||
covers_url="https://%s/images/" % DOMAIN,
|
||||
search_url="https://%s/search?q=" % DOMAIN,
|
||||
isbn_search_url="https://%s/isbn/" % DOMAIN,
|
||||
priority=1,
|
||||
)
|
||||
|
||||
Connector.objects.create(
|
||||
identifier='bookwyrm.social',
|
||||
name='BookWyrm dot Social',
|
||||
connector_file='bookwyrm_connector',
|
||||
base_url='https://bookwyrm.social',
|
||||
books_url='https://bookwyrm.social/book',
|
||||
covers_url='https://bookwyrm.social/images/covers',
|
||||
search_url='https://bookwyrm.social/search?q=',
|
||||
identifier="bookwyrm.social",
|
||||
name="BookWyrm dot Social",
|
||||
connector_file="bookwyrm_connector",
|
||||
base_url="https://bookwyrm.social",
|
||||
books_url="https://bookwyrm.social/book",
|
||||
covers_url="https://bookwyrm.social/images/",
|
||||
search_url="https://bookwyrm.social/search?q=",
|
||||
isbn_search_url="https://bookwyrm.social/isbn/",
|
||||
priority=2,
|
||||
)
|
||||
|
||||
Connector.objects.create(
|
||||
identifier='openlibrary.org',
|
||||
name='OpenLibrary',
|
||||
connector_file='openlibrary',
|
||||
base_url='https://openlibrary.org',
|
||||
books_url='https://openlibrary.org',
|
||||
covers_url='https://covers.openlibrary.org',
|
||||
search_url='https://openlibrary.org/search?q=',
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://openlibrary.org",
|
||||
books_url="https://openlibrary.org",
|
||||
covers_url="https://covers.openlibrary.org",
|
||||
search_url="https://openlibrary.org/search?q=",
|
||||
isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:",
|
||||
priority=3,
|
||||
)
|
||||
|
||||
|
||||
def init_federated_servers():
|
||||
""" big no to nazis """
|
||||
built_in_blocks = ["gab.ai", "gab.com"]
|
||||
for server in built_in_blocks:
|
||||
FederatedServer.objects.create(
|
||||
server_name=server,
|
||||
status="blocked",
|
||||
)
|
||||
|
||||
|
||||
def init_settings():
|
||||
SiteSettings.objects.create()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Initializes the database with starter data'
|
||||
help = "Initializes the database with starter data"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
init_groups()
|
||||
init_permissions()
|
||||
init_connectors()
|
||||
init_federated_servers()
|
||||
init_settings()
|
||||
|
|
30
bookwyrm/management/commands/populate_streams.py
Normal file
30
bookwyrm/management/commands/populate_streams.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
""" Re-create user streams """
|
||||
from django.core.management.base import BaseCommand
|
||||
import redis
|
||||
|
||||
from bookwyrm import activitystreams, models, settings
|
||||
|
||||
r = redis.Redis(
|
||||
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0
|
||||
)
|
||||
|
||||
|
||||
def populate_streams():
|
||||
""" build all the streams for all the users """
|
||||
users = models.User.objects.filter(
|
||||
local=True,
|
||||
is_active=True,
|
||||
)
|
||||
for user in users:
|
||||
for stream in activitystreams.streams.values():
|
||||
stream.populate_streams(user)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
""" start all over with user streams """
|
||||
|
||||
help = "Populate streams for all users"
|
||||
# pylint: disable=no-self-use,unused-argument
|
||||
def handle(self, *args, **options):
|
||||
""" run feed builder """
|
||||
populate_streams()
|
|
@ -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.db.models import Count, Q
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
def remove_editions():
|
||||
''' combine duplicate editions and update related models '''
|
||||
""" combine duplicate editions and update related models """
|
||||
# not in use
|
||||
filters = {'%s__isnull' % r.name: True \
|
||||
for r in models.Edition._meta.related_objects}
|
||||
filters = {
|
||||
"%s__isnull" % r.name: True for r in models.Edition._meta.related_objects
|
||||
}
|
||||
# no cover, no identifying fields
|
||||
filters['cover'] = ''
|
||||
null_fields = {'%s__isnull' % f: True for f in \
|
||||
['isbn_10', 'isbn_13', 'oclc_number']}
|
||||
filters["cover"] = ""
|
||||
null_fields = {
|
||||
"%s__isnull" % f: True for f in ["isbn_10", "isbn_13", "oclc_number"]
|
||||
}
|
||||
|
||||
editions = models.Edition.objects.filter(
|
||||
Q(languages=[]) | Q(languages__contains=['English']),
|
||||
**filters, **null_fields
|
||||
).annotate(Count('parent_work__editions')).filter(
|
||||
# mustn't be the only edition for the work
|
||||
parent_work__editions__count__gt=1
|
||||
editions = (
|
||||
models.Edition.objects.filter(
|
||||
Q(languages=[]) | Q(languages__contains=["English"]),
|
||||
**filters,
|
||||
**null_fields
|
||||
)
|
||||
.annotate(Count("parent_work__editions"))
|
||||
.filter(
|
||||
# mustn't be the only edition for the work
|
||||
parent_work__editions__count__gt=1
|
||||
)
|
||||
)
|
||||
print(editions.count())
|
||||
editions.delete()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
''' dedplucate allllll the book data models '''
|
||||
help = 'merges duplicate book data'
|
||||
""" dedplucate allllll the book data models """
|
||||
|
||||
help = "merges duplicate book data"
|
||||
# pylint: disable=no-self-use,unused-argument
|
||||
def handle(self, *args, **options):
|
||||
''' run deudplications '''
|
||||
""" run deudplications """
|
||||
remove_editions()
|
||||
|
|
|
@ -15,199 +15,448 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0011_update_proxy_permissions'),
|
||||
("auth", "0011_update_proxy_permissions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
name="User",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('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/')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"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={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Author',
|
||||
name="Author",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, 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()),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Book',
|
||||
name="Book",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, 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, 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')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
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, 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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FederatedServer',
|
||||
name="FederatedServer",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=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)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Shelf',
|
||||
name="Shelf",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=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)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=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(
|
||||
name='Status',
|
||||
name="Status",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content', models.TextField(blank=True, null=True)),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('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)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("content", models.TextField(blank=True, null=True)),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserRelationship',
|
||||
name="UserRelationship",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, 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.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)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
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.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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ShelfBook',
|
||||
name="ShelfBook",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, 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')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
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={
|
||||
'unique_together': {('book', 'shelf')},
|
||||
"unique_together": {("book", "shelf")},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shelf',
|
||||
name='books',
|
||||
field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Book'),
|
||||
model_name="shelf",
|
||||
name="books",
|
||||
field=models.ManyToManyField(
|
||||
through="bookwyrm.ShelfBook", to="bookwyrm.Book"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shelf',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="shelf",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='shelves',
|
||||
field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Shelf'),
|
||||
model_name="book",
|
||||
name="shelves",
|
||||
field=models.ManyToManyField(
|
||||
through="bookwyrm.ShelfBook", to="bookwyrm.Shelf"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='federated_server',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.FederatedServer'),
|
||||
model_name="user",
|
||||
name="federated_server",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="bookwyrm.FederatedServer",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='followers',
|
||||
field=models.ManyToManyField(through='bookwyrm.UserRelationship', to=settings.AUTH_USER_MODEL),
|
||||
model_name="user",
|
||||
name="followers",
|
||||
field=models.ManyToManyField(
|
||||
through="bookwyrm.UserRelationship", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
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'),
|
||||
model_name="user",
|
||||
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(
|
||||
model_name='user',
|
||||
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'),
|
||||
model_name="user",
|
||||
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(
|
||||
name='shelf',
|
||||
unique_together={('user', 'identifier')},
|
||||
name="shelf",
|
||||
unique_together={("user", "identifier")},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Review',
|
||||
name="Review",
|
||||
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)),
|
||||
('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')),
|
||||
(
|
||||
"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)),
|
||||
(
|
||||
"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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=('bookwyrm.status',),
|
||||
bases=("bookwyrm.status",),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,31 +8,59 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0001_initial'),
|
||||
("bookwyrm", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Favorite',
|
||||
name="Favorite",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, 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)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
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={
|
||||
'unique_together': {('user', 'status')},
|
||||
"unique_together": {("user", "status")},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='status',
|
||||
name='favorites',
|
||||
field=models.ManyToManyField(related_name='user_favorites', through='bookwyrm.Favorite', to=settings.AUTH_USER_MODEL),
|
||||
model_name="status",
|
||||
name="favorites",
|
||||
field=models.ManyToManyField(
|
||||
related_name="user_favorites",
|
||||
through="bookwyrm.Favorite",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='favorites',
|
||||
field=models.ManyToManyField(related_name='favorite_statuses', through='bookwyrm.Favorite', to='bookwyrm.Status'),
|
||||
model_name="user",
|
||||
name="favorites",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0002_auto_20200219_0816'),
|
||||
("bookwyrm", "0002_auto_20200219_0816"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='content',
|
||||
model_name="author",
|
||||
name="content",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='content',
|
||||
model_name="book",
|
||||
name="content",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='favorite',
|
||||
name='content',
|
||||
model_name="favorite",
|
||||
name="content",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='federatedserver',
|
||||
name='content',
|
||||
model_name="federatedserver",
|
||||
name="content",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='shelf',
|
||||
name='content',
|
||||
model_name="shelf",
|
||||
name="content",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='shelfbook',
|
||||
name='content',
|
||||
model_name="shelfbook",
|
||||
name="content",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='userrelationship',
|
||||
name='content',
|
||||
model_name="userrelationship",
|
||||
name="content",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='updated_date',
|
||||
model_name="author",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='updated_date',
|
||||
model_name="book",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='favorite',
|
||||
name='updated_date',
|
||||
model_name="favorite",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='federatedserver',
|
||||
name='updated_date',
|
||||
model_name="federatedserver",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shelf',
|
||||
name='updated_date',
|
||||
model_name="shelf",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shelfbook',
|
||||
name='updated_date',
|
||||
model_name="shelfbook",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='status',
|
||||
name='updated_date',
|
||||
model_name="status",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='created_date',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
model_name="user",
|
||||
name="created_date",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='updated_date',
|
||||
model_name="user",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userrelationship',
|
||||
name='updated_date',
|
||||
model_name="userrelationship",
|
||||
name="updated_date",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,22 +8,41 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0003_auto_20200221_0131'),
|
||||
("bookwyrm", "0003_auto_20200221_0131"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
name="Tag",
|
||||
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)),
|
||||
('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)),
|
||||
(
|
||||
"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)),
|
||||
("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={
|
||||
'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):
|
||||
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):
|
||||
tag.identifier = re.sub(r'\W+', '-', tag.name).lower()
|
||||
tag.identifier = re.sub(r"\W+", "-", tag.name).lower()
|
||||
tag.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0004_tag'),
|
||||
("bookwyrm", "0004_tag"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='identifier',
|
||||
model_name="tag",
|
||||
name="identifier",
|
||||
field=models.CharField(max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='name',
|
||||
model_name="tag",
|
||||
name="name",
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0006_auto_20200221_1702_squashed_0064_merge_20201101_1913'),
|
||||
("bookwyrm", "0006_auto_20200221_1702_squashed_0064_merge_20201101_1913"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='siteinvite',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
model_name="siteinvite",
|
||||
name="user",
|
||||
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):
|
||||
db_alias = schema_editor.connection.alias
|
||||
works = app_registry.get_model('bookwyrm', 'Work').objects.using(db_alias)
|
||||
editions = app_registry.get_model('bookwyrm', 'Edition').objects.using(db_alias)
|
||||
works = app_registry.get_model("bookwyrm", "Work").objects.using(db_alias)
|
||||
editions = app_registry.get_model("bookwyrm", "Edition").objects.using(db_alias)
|
||||
for work in works:
|
||||
ed = editions.filter(parent_work=work, default=True).first()
|
||||
if not ed:
|
||||
|
@ -15,21 +15,26 @@ def set_default_edition(app_registry, schema_editor):
|
|||
work.default_edition = ed
|
||||
work.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0007_auto_20201103_0014'),
|
||||
("bookwyrm", "0007_auto_20201103_0014"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='work',
|
||||
name='default_edition',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
model_name="work",
|
||||
name="default_edition",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="bookwyrm.Edition",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(set_default_edition),
|
||||
migrations.RemoveField(
|
||||
model_name='edition',
|
||||
name='default',
|
||||
model_name="edition",
|
||||
name="default",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,13 +6,22 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0008_work_default_edition'),
|
||||
("bookwyrm", "0008_work_default_edition"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shelf',
|
||||
name='privacy',
|
||||
field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
||||
model_name="shelf",
|
||||
name="privacy",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0009_shelf_privacy'),
|
||||
("bookwyrm", "0009_shelf_privacy"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='importjob',
|
||||
name='retry',
|
||||
model_name="importjob",
|
||||
name="retry",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_origin_id(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
books = app_registry.get_model('bookwyrm', 'Book').objects.using(db_alias)
|
||||
books = app_registry.get_model("bookwyrm", "Book").objects.using(db_alias)
|
||||
for book in books:
|
||||
book.origin_id = book.remote_id
|
||||
# the remote_id will be set automatically
|
||||
|
@ -15,18 +16,18 @@ def set_origin_id(app_registry, schema_editor):
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0010_importjob_retry'),
|
||||
("bookwyrm", "0010_importjob_retry"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='origin_id',
|
||||
model_name="author",
|
||||
name="origin_id",
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='origin_id',
|
||||
model_name="book",
|
||||
name="origin_id",
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.RunPython(set_origin_id),
|
||||
|
|
|
@ -7,23 +7,41 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0011_auto_20201113_1727'),
|
||||
("bookwyrm", "0011_auto_20201113_1727"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attachment',
|
||||
name="Attachment",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('remote_id', models.CharField(max_length=255, null=True)),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='status/')),
|
||||
('caption', models.TextField(blank=True, null=True)),
|
||||
('status', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_date", models.DateTimeField(auto_now=True)),
|
||||
("remote_id", models.CharField(max_length=255, null=True)),
|
||||
(
|
||||
"image",
|
||||
models.ImageField(blank=True, null=True, upload_to="status/"),
|
||||
),
|
||||
("caption", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"status",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="attachments",
|
||||
to="bookwyrm.Status",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,24 +8,51 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0011_auto_20201113_1727'),
|
||||
("bookwyrm", "0011_auto_20201113_1727"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProgressUpdate',
|
||||
name="ProgressUpdate",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('remote_id', models.CharField(max_length=255, null=True)),
|
||||
('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)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_date", models.DateTimeField(auto_now=True)),
|
||||
("remote_id", models.CharField(max_length=255, null=True)),
|
||||
("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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0012_attachment'),
|
||||
("bookwyrm", "0012_attachment"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='origin_id',
|
||||
model_name="book",
|
||||
name="origin_id",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,12 +6,12 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0013_book_origin_id'),
|
||||
("bookwyrm", "0013_book_origin_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Attachment',
|
||||
new_name='Image',
|
||||
old_name="Attachment",
|
||||
new_name="Image",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,9 +6,8 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0013_book_origin_id'),
|
||||
('bookwyrm', '0012_progressupdate'),
|
||||
("bookwyrm", "0013_book_origin_id"),
|
||||
("bookwyrm", "0012_progressupdate"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
operations = []
|
||||
|
|
|
@ -7,13 +7,18 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0014_auto_20201128_0118'),
|
||||
("bookwyrm", "0014_auto_20201128_0118"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='status',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='bookwyrm.Status'),
|
||||
model_name="image",
|
||||
name="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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0014_merge_20201128_0007'),
|
||||
("bookwyrm", "0014_merge_20201128_0007"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='readthrough',
|
||||
old_name='pages_read',
|
||||
new_name='progress',
|
||||
model_name="readthrough",
|
||||
old_name="pages_read",
|
||||
new_name="progress",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='readthrough',
|
||||
name='progress_mode',
|
||||
field=models.CharField(choices=[('PG', 'page'), ('PCT', 'percent')], default='PG', max_length=3),
|
||||
model_name="readthrough",
|
||||
name="progress_mode",
|
||||
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
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0015_auto_20201128_0349'),
|
||||
("bookwyrm", "0015_auto_20201128_0349"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subject_places',
|
||||
field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
||||
model_name="book",
|
||||
name="subject_places",
|
||||
field=ArrayField(
|
||||
base_field=models.CharField(max_length=255),
|
||||
blank=True,
|
||||
default=list,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subjects',
|
||||
field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
||||
model_name="book",
|
||||
name="subjects",
|
||||
field=ArrayField(
|
||||
base_field=models.CharField(max_length=255),
|
||||
blank=True,
|
||||
default=list,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='parent_work',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='editions', to='bookwyrm.Work'),
|
||||
model_name="edition",
|
||||
name="parent_work",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="editions",
|
||||
to="bookwyrm.Work",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='name',
|
||||
model_name="tag",
|
||||
name="name",
|
||||
field=models.CharField(max_length=100, unique=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='tag',
|
||||
name="tag",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tag',
|
||||
name='book',
|
||||
model_name="tag",
|
||||
name="book",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tag',
|
||||
name='user',
|
||||
model_name="tag",
|
||||
name="user",
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserTag',
|
||||
name="UserTag",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('remote_id', models.CharField(max_length=255, null=True)),
|
||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')),
|
||||
('tag', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Tag')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_date", models.DateTimeField(auto_now=True)),
|
||||
("remote_id", models.CharField(max_length=255, null=True)),
|
||||
(
|
||||
"book",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="bookwyrm.Edition",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tag",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Tag"
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'book', 'tag')},
|
||||
"unique_together": {("user", "book", "tag")},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,23 +6,23 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0015_auto_20201128_0349'),
|
||||
("bookwyrm", "0015_auto_20201128_0349"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='admin_email',
|
||||
model_name="sitesettings",
|
||||
name="admin_email",
|
||||
field=models.EmailField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='support_link',
|
||||
model_name="sitesettings",
|
||||
name="support_link",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='support_title',
|
||||
model_name="sitesettings",
|
||||
name="support_title",
|
||||
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
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def copy_rsa_keys(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
users = app_registry.get_model('bookwyrm', 'User')
|
||||
keypair = app_registry.get_model('bookwyrm', 'KeyPair')
|
||||
users = app_registry.get_model("bookwyrm", "User")
|
||||
keypair = app_registry.get_model("bookwyrm", "KeyPair")
|
||||
for user in users.objects.using(db_alias):
|
||||
if user.public_key or user.private_key:
|
||||
user.key_pair = keypair.objects.create(
|
||||
remote_id='%s/#main-key' % user.remote_id,
|
||||
remote_id="%s/#main-key" % user.remote_id,
|
||||
private_key=user.private_key,
|
||||
public_key=user.public_key
|
||||
public_key=user.public_key,
|
||||
)
|
||||
user.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0016_auto_20201129_0304'),
|
||||
("bookwyrm", "0016_auto_20201129_0304"),
|
||||
]
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KeyPair',
|
||||
name="KeyPair",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
|
||||
('private_key', models.TextField(blank=True, null=True)),
|
||||
('public_key', bookwyrm.models.fields.TextField(blank=True, null=True)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_date", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"remote_id",
|
||||
bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
("private_key", models.TextField(blank=True, null=True)),
|
||||
("public_key", bookwyrm.models.fields.TextField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='followers',
|
||||
field=bookwyrm.models.fields.ManyToManyField(related_name='following', through='bookwyrm.UserFollows', to=settings.AUTH_USER_MODEL),
|
||||
model_name="user",
|
||||
name="followers",
|
||||
field=bookwyrm.models.fields.ManyToManyField(
|
||||
related_name="following",
|
||||
through="bookwyrm.UserFollows",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="author",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="book",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='connector',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="connector",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='favorite',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="favorite",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='federatedserver',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="federatedserver",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="image",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="notification",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='readthrough',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="readthrough",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="shelf",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="shelfbook",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="status",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="tag",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='avatar',
|
||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='avatars/'),
|
||||
model_name="user",
|
||||
name="avatar",
|
||||
field=bookwyrm.models.fields.ImageField(
|
||||
blank=True, null=True, upload_to="avatars/"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='bookwyrm_user',
|
||||
model_name="user",
|
||||
name="bookwyrm_user",
|
||||
field=bookwyrm.models.fields.BooleanField(default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='inbox',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="user",
|
||||
name="inbox",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='local',
|
||||
model_name="user",
|
||||
name="local",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='manually_approves_followers',
|
||||
model_name="user",
|
||||
name="manually_approves_followers",
|
||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=100, null=True),
|
||||
model_name="user",
|
||||
name="name",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=100, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='outbox',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="user",
|
||||
name="outbox",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="user",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
unique=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='shared_inbox',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="user",
|
||||
name="shared_inbox",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='summary',
|
||||
model_name="user",
|
||||
name="summary",
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='username',
|
||||
model_name="user",
|
||||
name="username",
|
||||
field=bookwyrm.models.fields.UsernameField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userblocks',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="userblocks",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollowrequest',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="userfollowrequest",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollows',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="userfollows",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usertag',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="usertag",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='key_pair',
|
||||
field=bookwyrm.models.fields.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to='bookwyrm.KeyPair'),
|
||||
model_name="user",
|
||||
name="key_pair",
|
||||
field=bookwyrm.models.fields.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="owner",
|
||||
to="bookwyrm.KeyPair",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(copy_rsa_keys),
|
||||
]
|
||||
|
|
|
@ -7,13 +7,15 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0016_auto_20201211_2026'),
|
||||
("bookwyrm", "0016_auto_20201211_2026"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='readthrough',
|
||||
name='book',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
model_name="readthrough",
|
||||
name="book",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0017_auto_20201130_1819'),
|
||||
("bookwyrm", "0017_auto_20201130_1819"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='following',
|
||||
model_name="user",
|
||||
name="following",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='private_key',
|
||||
model_name="user",
|
||||
name="private_key",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='public_key',
|
||||
model_name="user",
|
||||
name="public_key",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -3,34 +3,36 @@
|
|||
import bookwyrm.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_notnull(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
users = app_registry.get_model('bookwyrm', 'User')
|
||||
users = app_registry.get_model("bookwyrm", "User")
|
||||
for user in users.objects.using(db_alias):
|
||||
if user.name and user.summary:
|
||||
continue
|
||||
if not user.summary:
|
||||
user.summary = ''
|
||||
user.summary = ""
|
||||
if not user.name:
|
||||
user.name = ''
|
||||
user.name = ""
|
||||
user.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0018_auto_20201130_1832'),
|
||||
("bookwyrm", "0018_auto_20201130_1832"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_notnull),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(default='', max_length=100),
|
||||
model_name="user",
|
||||
name="name",
|
||||
field=bookwyrm.models.fields.CharField(default="", max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='summary',
|
||||
field=bookwyrm.models.fields.TextField(default=''),
|
||||
model_name="user",
|
||||
name="summary",
|
||||
field=bookwyrm.models.fields.TextField(default=""),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -11,343 +11,497 @@ import django.utils.timezone
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0019_auto_20201130_1939'),
|
||||
("bookwyrm", "0019_auto_20201130_1939"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='aliases',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
model_name="author",
|
||||
name="aliases",
|
||||
field=bookwyrm.models.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=255),
|
||||
blank=True,
|
||||
default=list,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='bio',
|
||||
model_name="author",
|
||||
name="bio",
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='born',
|
||||
model_name="author",
|
||||
name="born",
|
||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='died',
|
||||
model_name="author",
|
||||
name="died",
|
||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='name',
|
||||
model_name="author",
|
||||
name="name",
|
||||
field=bookwyrm.models.fields.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='openlibrary_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="author",
|
||||
name="openlibrary_key",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='wikipedia_link',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="author",
|
||||
name="wikipedia_link",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='authors',
|
||||
field=bookwyrm.models.fields.ManyToManyField(to='bookwyrm.Author'),
|
||||
model_name="book",
|
||||
name="authors",
|
||||
field=bookwyrm.models.fields.ManyToManyField(to="bookwyrm.Author"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='cover',
|
||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='covers/'),
|
||||
model_name="book",
|
||||
name="cover",
|
||||
field=bookwyrm.models.fields.ImageField(
|
||||
blank=True, null=True, upload_to="covers/"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='description',
|
||||
model_name="book",
|
||||
name="description",
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='first_published_date',
|
||||
model_name="book",
|
||||
name="first_published_date",
|
||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='goodreads_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="book",
|
||||
name="goodreads_key",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='languages',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
model_name="book",
|
||||
name="languages",
|
||||
field=bookwyrm.models.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=255),
|
||||
blank=True,
|
||||
default=list,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='librarything_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="book",
|
||||
name="librarything_key",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='openlibrary_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="book",
|
||||
name="openlibrary_key",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='published_date',
|
||||
model_name="book",
|
||||
name="published_date",
|
||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='series',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="book",
|
||||
name="series",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='series_number',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="book",
|
||||
name="series_number",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='sort_title',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="book",
|
||||
name="sort_title",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subject_places',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
||||
model_name="book",
|
||||
name="subject_places",
|
||||
field=bookwyrm.models.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=255),
|
||||
blank=True,
|
||||
default=list,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subjects',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, null=True, size=None),
|
||||
model_name="book",
|
||||
name="subjects",
|
||||
field=bookwyrm.models.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=255),
|
||||
blank=True,
|
||||
default=list,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='subtitle',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="book",
|
||||
name="subtitle",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='title',
|
||||
model_name="book",
|
||||
name="title",
|
||||
field=bookwyrm.models.fields.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boost',
|
||||
name='boosted_status',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boosters', to='bookwyrm.Status'),
|
||||
model_name="boost",
|
||||
name="boosted_status",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="boosters",
|
||||
to="bookwyrm.Status",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
model_name="comment",
|
||||
name="book",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='asin',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="edition",
|
||||
name="asin",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='isbn_10',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="edition",
|
||||
name="isbn_10",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='isbn_13',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="edition",
|
||||
name="isbn_13",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='oclc_number',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="edition",
|
||||
name="oclc_number",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='pages',
|
||||
model_name="edition",
|
||||
name="pages",
|
||||
field=bookwyrm.models.fields.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='parent_work',
|
||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='editions', to='bookwyrm.Work'),
|
||||
model_name="edition",
|
||||
name="parent_work",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="editions",
|
||||
to="bookwyrm.Work",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='physical_format',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="edition",
|
||||
name="physical_format",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='publishers',
|
||||
field=bookwyrm.models.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
|
||||
model_name="edition",
|
||||
name="publishers",
|
||||
field=bookwyrm.models.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=255),
|
||||
blank=True,
|
||||
default=list,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='favorite',
|
||||
name='status',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'),
|
||||
model_name="favorite",
|
||||
name="status",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Status"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='favorite',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="favorite",
|
||||
name="user",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='caption',
|
||||
model_name="image",
|
||||
name="caption",
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='image',
|
||||
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='status/'),
|
||||
model_name="image",
|
||||
name="image",
|
||||
field=bookwyrm.models.fields.ImageField(
|
||||
blank=True, null=True, upload_to="status/"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotation',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
model_name="quotation",
|
||||
name="book",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotation',
|
||||
name='quote',
|
||||
model_name="quotation",
|
||||
name="quote",
|
||||
field=bookwyrm.models.fields.TextField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
model_name="review",
|
||||
name="book",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='name',
|
||||
model_name="review",
|
||||
name="name",
|
||||
field=bookwyrm.models.fields.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='rating',
|
||||
field=bookwyrm.models.fields.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]),
|
||||
model_name="review",
|
||||
name="rating",
|
||||
field=bookwyrm.models.fields.IntegerField(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(5),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='name',
|
||||
model_name="shelf",
|
||||
name="name",
|
||||
field=bookwyrm.models.fields.CharField(max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='privacy',
|
||||
field=bookwyrm.models.fields.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
||||
model_name="shelf",
|
||||
name="privacy",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("unlisted", "Unlisted"),
|
||||
("followers", "Followers"),
|
||||
("direct", "Direct"),
|
||||
],
|
||||
default="public",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="shelf",
|
||||
name="user",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='added_by',
|
||||
field=bookwyrm.models.fields.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="shelfbook",
|
||||
name="added_by",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
model_name="shelfbook",
|
||||
name="book",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='shelf',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Shelf'),
|
||||
model_name="shelfbook",
|
||||
name="shelf",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Shelf"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='content',
|
||||
model_name="status",
|
||||
name="content",
|
||||
field=bookwyrm.models.fields.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='mention_books',
|
||||
field=bookwyrm.models.fields.TagField(related_name='mention_book', to='bookwyrm.Edition'),
|
||||
model_name="status",
|
||||
name="mention_books",
|
||||
field=bookwyrm.models.fields.TagField(
|
||||
related_name="mention_book", to="bookwyrm.Edition"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='mention_users',
|
||||
field=bookwyrm.models.fields.TagField(related_name='mention_user', to=settings.AUTH_USER_MODEL),
|
||||
model_name="status",
|
||||
name="mention_users",
|
||||
field=bookwyrm.models.fields.TagField(
|
||||
related_name="mention_user", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='published_date',
|
||||
field=bookwyrm.models.fields.DateTimeField(default=django.utils.timezone.now),
|
||||
model_name="status",
|
||||
name="published_date",
|
||||
field=bookwyrm.models.fields.DateTimeField(
|
||||
default=django.utils.timezone.now
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='reply_parent',
|
||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status'),
|
||||
model_name="status",
|
||||
name="reply_parent",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="bookwyrm.Status",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='sensitive',
|
||||
model_name="status",
|
||||
name="sensitive",
|
||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="status",
|
||||
name="user",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='name',
|
||||
model_name="tag",
|
||||
name="name",
|
||||
field=bookwyrm.models.fields.CharField(max_length=100, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userblocks',
|
||||
name='user_object',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_object', to=settings.AUTH_USER_MODEL),
|
||||
model_name="userblocks",
|
||||
name="user_object",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="userblocks_user_object",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userblocks',
|
||||
name='user_subject',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
model_name="userblocks",
|
||||
name="user_subject",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="userblocks_user_subject",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollowrequest',
|
||||
name='user_object',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_object', to=settings.AUTH_USER_MODEL),
|
||||
model_name="userfollowrequest",
|
||||
name="user_object",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="userfollowrequest_user_object",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollowrequest',
|
||||
name='user_subject',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
model_name="userfollowrequest",
|
||||
name="user_subject",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="userfollowrequest_user_subject",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollows',
|
||||
name='user_object',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_object', to=settings.AUTH_USER_MODEL),
|
||||
model_name="userfollows",
|
||||
name="user_object",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="userfollows_user_object",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollows',
|
||||
name='user_subject',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_subject', to=settings.AUTH_USER_MODEL),
|
||||
model_name="userfollows",
|
||||
name="user_subject",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="userfollows_user_subject",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usertag',
|
||||
name='book',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
model_name="usertag",
|
||||
name="book",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usertag',
|
||||
name='tag',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Tag'),
|
||||
model_name="usertag",
|
||||
name="tag",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Tag"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usertag',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="usertag",
|
||||
name="user",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='default_edition',
|
||||
field=bookwyrm.models.fields.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'),
|
||||
model_name="work",
|
||||
name="default_edition",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="bookwyrm.Edition",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='lccn',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="work",
|
||||
name="lccn",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0020_auto_20201208_0213'),
|
||||
('bookwyrm', '0016_auto_20201211_2026'),
|
||||
("bookwyrm", "0020_auto_20201208_0213"),
|
||||
("bookwyrm", "0016_auto_20201211_2026"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
operations = []
|
||||
|
|
|
@ -5,26 +5,27 @@ from django.db import migrations
|
|||
|
||||
def set_author_name(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
authors = app_registry.get_model('bookwyrm', 'Author')
|
||||
authors = app_registry.get_model("bookwyrm", "Author")
|
||||
for author in authors.objects.using(db_alias):
|
||||
if not author.name:
|
||||
author.name = '%s %s' % (author.first_name, author.last_name)
|
||||
author.name = "%s %s" % (author.first_name, author.last_name)
|
||||
author.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0021_merge_20201212_1737'),
|
||||
("bookwyrm", "0021_merge_20201212_1737"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_author_name),
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='first_name',
|
||||
model_name="author",
|
||||
name="first_name",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='last_name',
|
||||
model_name="author",
|
||||
name="last_name",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,13 +7,22 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0022_auto_20201212_1744'),
|
||||
("bookwyrm", "0022_auto_20201212_1744"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='privacy',
|
||||
field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
||||
model_name="status",
|
||||
name="privacy",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0017_auto_20201212_0059'),
|
||||
('bookwyrm', '0022_auto_20201212_1744'),
|
||||
("bookwyrm", "0017_auto_20201212_0059"),
|
||||
("bookwyrm", "0022_auto_20201212_1744"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
operations = []
|
||||
|
|
|
@ -6,9 +6,8 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0023_auto_20201214_0511'),
|
||||
('bookwyrm', '0023_merge_20201216_0112'),
|
||||
("bookwyrm", "0023_auto_20201214_0511"),
|
||||
("bookwyrm", "0023_merge_20201216_0112"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
operations = []
|
||||
|
|
|
@ -7,33 +7,33 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0024_merge_20201216_1721'),
|
||||
("bookwyrm", "0024_merge_20201216_1721"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='bio',
|
||||
model_name="author",
|
||||
name="bio",
|
||||
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='book',
|
||||
name='description',
|
||||
model_name="book",
|
||||
name="description",
|
||||
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotation',
|
||||
name='quote',
|
||||
model_name="quotation",
|
||||
name="quote",
|
||||
field=bookwyrm.models.fields.HtmlField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='status',
|
||||
name='content',
|
||||
model_name="status",
|
||||
name="content",
|
||||
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='summary',
|
||||
field=bookwyrm.models.fields.HtmlField(default=''),
|
||||
model_name="user",
|
||||
name="summary",
|
||||
field=bookwyrm.models.fields.HtmlField(default=""),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,13 +7,15 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0025_auto_20201217_0046'),
|
||||
("bookwyrm", "0025_auto_20201217_0046"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='status',
|
||||
name='content_warning',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=500, null=True),
|
||||
model_name="status",
|
||||
name="content_warning",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0026_status_content_warning'),
|
||||
("bookwyrm", "0026_status_content_warning"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='name',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=100, null=True),
|
||||
model_name="user",
|
||||
name="name",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=100, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='summary',
|
||||
model_name="user",
|
||||
name="summary",
|
||||
field=bookwyrm.models.fields.HtmlField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,12 +6,12 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0027_auto_20201220_2007'),
|
||||
("bookwyrm", "0027_auto_20201220_2007"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='author_text',
|
||||
model_name="book",
|
||||
name="author_text",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -9,53 +9,65 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0028_remove_book_author_text'),
|
||||
("bookwyrm", "0028_remove_book_author_text"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='last_sync_date',
|
||||
model_name="author",
|
||||
name="last_sync_date",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='author',
|
||||
name='sync',
|
||||
model_name="author",
|
||||
name="sync",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='last_sync_date',
|
||||
model_name="book",
|
||||
name="last_sync_date",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='sync',
|
||||
model_name="book",
|
||||
name="sync",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='book',
|
||||
name='sync_cover',
|
||||
model_name="book",
|
||||
name="sync_cover",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='goodreads_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="author",
|
||||
name="goodreads_key",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='last_edited_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="author",
|
||||
name="last_edited_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='author',
|
||||
name='librarything_key',
|
||||
field=bookwyrm.models.fields.CharField(blank=True, max_length=255, null=True),
|
||||
model_name="author",
|
||||
name="librarything_key",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='book',
|
||||
name='last_edited_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="book",
|
||||
name="last_edited_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='author',
|
||||
name='origin_id',
|
||||
model_name="author",
|
||||
name="origin_id",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,13 +7,18 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0029_auto_20201221_2014'),
|
||||
("bookwyrm", "0029_auto_20201221_2014"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='localname',
|
||||
field=models.CharField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_localname]),
|
||||
model_name="user",
|
||||
name="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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0030_auto_20201224_1939'),
|
||||
("bookwyrm", "0030_auto_20201224_1939"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='favicon',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='logos/'),
|
||||
model_name="sitesettings",
|
||||
name="favicon",
|
||||
field=models.ImageField(blank=True, null=True, upload_to="logos/"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='logo',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='logos/'),
|
||||
model_name="sitesettings",
|
||||
name="logo",
|
||||
field=models.ImageField(blank=True, null=True, upload_to="logos/"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='logo_small',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='logos/'),
|
||||
model_name="sitesettings",
|
||||
name="logo_small",
|
||||
field=models.ImageField(blank=True, null=True, upload_to="logos/"),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,18 +6,20 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0031_auto_20210104_2040'),
|
||||
("bookwyrm", "0031_auto_20210104_2040"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='instance_tagline',
|
||||
field=models.CharField(default='Social Reading and Reviewing', max_length=150),
|
||||
model_name="sitesettings",
|
||||
name="instance_tagline",
|
||||
field=models.CharField(
|
||||
default="Social Reading and Reviewing", max_length=150
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='registration_closed_text',
|
||||
field=models.TextField(default='Contact an administrator to get an invite'),
|
||||
model_name="sitesettings",
|
||||
name="registration_closed_text",
|
||||
field=models.TextField(default="Contact an administrator to get an invite"),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,14 +7,16 @@ import django.utils.timezone
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0032_auto_20210104_2055'),
|
||||
("bookwyrm", "0032_auto_20210104_2055"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='siteinvite',
|
||||
name='created_date',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
model_name="siteinvite",
|
||||
name="created_date",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0033_siteinvite_created_date'),
|
||||
("bookwyrm", "0033_siteinvite_created_date"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='importjob',
|
||||
name='complete',
|
||||
model_name="importjob",
|
||||
name="complete",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,20 +6,21 @@ from django.db import migrations
|
|||
|
||||
def set_rank(app_registry, schema_editor):
|
||||
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):
|
||||
book.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0034_importjob_complete'),
|
||||
("bookwyrm", "0034_importjob_complete"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='edition_rank',
|
||||
model_name="edition",
|
||||
name="edition_rank",
|
||||
field=bookwyrm.models.fields.IntegerField(default=0),
|
||||
),
|
||||
migrations.RunPython(set_rank),
|
||||
|
|
|
@ -9,24 +9,57 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0035_edition_edition_rank'),
|
||||
("bookwyrm", "0035_edition_edition_rank"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AnnualGoal',
|
||||
name="AnnualGoal",
|
||||
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])),
|
||||
('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)),
|
||||
(
|
||||
"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],
|
||||
),
|
||||
),
|
||||
("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={
|
||||
'unique_together': {('user', 'year')},
|
||||
"unique_together": {("user", "year")},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -2,36 +2,39 @@
|
|||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def empty_to_null(apps, schema_editor):
|
||||
User = apps.get_model("bookwyrm", "User")
|
||||
db_alias = schema_editor.connection.alias
|
||||
User.objects.using(db_alias).filter(email="").update(email=None)
|
||||
|
||||
|
||||
def null_to_empty(apps, schema_editor):
|
||||
User = apps.get_model("bookwyrm", "User")
|
||||
db_alias = schema_editor.connection.alias
|
||||
User.objects.using(db_alias).filter(email=None).update(email="")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0036_annualgoal'),
|
||||
("bookwyrm", "0036_annualgoal"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='shelfbook',
|
||||
options={'ordering': ('-created_date',)},
|
||||
name="shelfbook",
|
||||
options={"ordering": ("-created_date",)},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='email',
|
||||
model_name="user",
|
||||
name="email",
|
||||
field=models.EmailField(max_length=254, null=True),
|
||||
),
|
||||
migrations.RunPython(empty_to_null, null_to_empty),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='email',
|
||||
model_name="user",
|
||||
name="email",
|
||||
field=models.EmailField(max_length=254, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,13 +7,15 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0037_auto_20210118_1954'),
|
||||
("bookwyrm", "0037_auto_20210118_1954"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='annualgoal',
|
||||
name='goal',
|
||||
field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)]),
|
||||
model_name="annualgoal",
|
||||
name="goal",
|
||||
field=models.IntegerField(
|
||||
validators=[django.core.validators.MinValueValidator(1)]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,9 +6,8 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0038_auto_20210119_1534'),
|
||||
('bookwyrm', '0015_auto_20201128_0734'),
|
||||
("bookwyrm", "0038_auto_20210119_1534"),
|
||||
("bookwyrm", "0015_auto_20201128_0734"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
operations = []
|
||||
|
|
|
@ -9,28 +9,40 @@ import django.db.models.deletion
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0039_merge_20210120_0753'),
|
||||
("bookwyrm", "0039_merge_20210120_0753"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='progressupdate',
|
||||
name='progress',
|
||||
field=models.IntegerField(validators=[django.core.validators.MinValueValidator(0)]),
|
||||
model_name="progressupdate",
|
||||
name="progress",
|
||||
field=models.IntegerField(
|
||||
validators=[django.core.validators.MinValueValidator(0)]
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='progressupdate',
|
||||
name='readthrough',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ReadThrough'),
|
||||
model_name="progressupdate",
|
||||
name="readthrough",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="bookwyrm.ReadThrough"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='progressupdate',
|
||||
name='remote_id',
|
||||
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
|
||||
model_name="progressupdate",
|
||||
name="remote_id",
|
||||
field=bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='readthrough',
|
||||
name='progress',
|
||||
field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
model_name="readthrough",
|
||||
name="progress",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0040_auto_20210122_0057'),
|
||||
("bookwyrm", "0040_auto_20210122_0057"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='List',
|
||||
name="List",
|
||||
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])),
|
||||
('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)),
|
||||
(
|
||||
"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],
|
||||
),
|
||||
),
|
||||
("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={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=(bookwyrm.models.activitypub_mixin.OrderedCollectionMixin, models.Model),
|
||||
bases=(
|
||||
bookwyrm.models.activitypub_mixin.OrderedCollectionMixin,
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ListItem',
|
||||
name="ListItem",
|
||||
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])),
|
||||
('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)),
|
||||
(
|
||||
"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],
|
||||
),
|
||||
),
|
||||
("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={
|
||||
'ordering': ('-created_date',),
|
||||
'unique_together': {('book', 'book_list')},
|
||||
"ordering": ("-created_date",),
|
||||
"unique_together": {("book", "book_list")},
|
||||
},
|
||||
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='list',
|
||||
name='books',
|
||||
field=models.ManyToManyField(through='bookwyrm.ListItem', to='bookwyrm.Edition'),
|
||||
model_name="list",
|
||||
name="books",
|
||||
field=models.ManyToManyField(
|
||||
through="bookwyrm.ListItem", to="bookwyrm.Edition"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='list',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="list",
|
||||
name="user",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0041_auto_20210131_1614'),
|
||||
("bookwyrm", "0041_auto_20210131_1614"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='list',
|
||||
options={'ordering': ('-updated_date',)},
|
||||
name="list",
|
||||
options={"ordering": ("-updated_date",)},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='list',
|
||||
name='privacy',
|
||||
field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
||||
model_name="list",
|
||||
name="privacy",
|
||||
field=bookwyrm.models.fields.PrivacyField(
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("unlisted", "Unlisted"),
|
||||
("followers", "Followers"),
|
||||
("direct", "Direct"),
|
||||
],
|
||||
default="public",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='shelf',
|
||||
name='privacy',
|
||||
field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
|
||||
model_name="shelf",
|
||||
name="privacy",
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0042_auto_20210201_2108'),
|
||||
("bookwyrm", "0042_auto_20210201_2108"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='listitem',
|
||||
old_name='added_by',
|
||||
new_name='user',
|
||||
model_name="listitem",
|
||||
old_name="added_by",
|
||||
new_name="user",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='shelfbook',
|
||||
old_name='added_by',
|
||||
new_name='user',
|
||||
model_name="shelfbook",
|
||||
old_name="added_by",
|
||||
new_name="user",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -5,9 +5,10 @@ from django.conf import settings
|
|||
from django.db import migrations
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def set_user(app_registry, schema_editor):
|
||||
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):
|
||||
item.user = item.shelf.user
|
||||
try:
|
||||
|
@ -19,15 +20,19 @@ def set_user(app_registry, schema_editor):
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0043_auto_20210204_2223'),
|
||||
("bookwyrm", "0043_auto_20210204_2223"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_user, lambda x, y: None),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(default=2, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
model_name="shelfbook",
|
||||
name="user",
|
||||
field=bookwyrm.models.fields.ForeignKey(
|
||||
default=2,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue