diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 000000000..de770ccee --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,13 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: psf/black@stable + with: + args: ". --check -l 80 -S" diff --git a/.gitignore b/.gitignore index 1384056f2..4b5b7fef2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /venv *.pyc *.swp +**/__pycache__ # VSCode /.vscode @@ -15,4 +16,4 @@ /images/ # Testing -.coverage \ No newline at end of file +.coverage diff --git a/README.md b/README.md index 41bd70653..414c01645 100644 --- a/README.md +++ b/README.md @@ -169,13 +169,17 @@ Instructions for running BookWyrm in production: - Set a secure database password for postgres - Update your nginx configuration in `nginx/default.conf` - Replace `your-domain.com` with your domain name + - If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`. + - If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy) - Run the application (this should also set up a Certbot ssl cert for your domain) with `docker-compose up --build`, and make sure all the images build successfully + - If you are running other services on your host machine, you may run into errors where services fail when attempting to bind to a port. + See the [troubleshooting guide](#port-conflicts) for advice on resolving this. - When docker has built successfully, stop the process with `CTRL-C` - - Comment out the `command: certonly...` line in `docker-compose.yml` + - Comment out the `command: certonly...` line in `docker-compose.yml`, and uncomment the following line (`command: renew ...`) so that the certificate will be automatically renewed. + - Uncomment the https redirect and `server` block in `nginx/default.conf` (lines 17-48). - Run docker-compose in the background with: `docker-compose up -d` - Initialize the database with: `./bw-dev initdb` - - Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U ` and saves the backup to a safe location Congrats! You did it, go to your domain and enjoy the fruits of your labors. @@ -202,6 +206,99 @@ 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. +### Backups + +BookWyrm's db service dumps a backup copy of its database to its `/backups` directory daily at midnight UTC. +Backups are named `backup__%Y-%m-%d.sql`. + +The db service has an optional script for periodically pruning the backups directory so that all recent daily backups are kept, but for older backups, only weekly or monthly backups are kept. +To enable this script: + - Uncomment the final line in `postgres-docker/cronfile` + - rebuild your instance `docker-compose up --build` + +You can copy backups from the backups volume to your host machine with `docker cp`: + - Run `docker-compose ps` to confirm the db service's full name (it's probably `bookwyrm_db_1`. + - Run `docker cp :/backups ` + +### Port Conflicts + +BookWyrm has multiple services that run on their default ports. +This means that, depending on what else you are running on your host machine, you may run into errors when building or running BookWyrm when attempts to bind to those ports fail. + +If this occurs, you will need to change your configuration to run services on different ports. +This may require one or more changes the following files: + - `docker-compose.yml` + - `nginx/default.conf` + - `.env` (You create this file yourself during setup) + +E.g., If you need Redis to run on a different port: + - In `docker-compose.yml`: + - In `services` -> `redis` -> `command`, add `--port YOUR_PORT` to the command + - In `services` -> `redis` -> `ports`, change `6379:6379` to your port + - In `.env`, update `REDIS_PORT` + +If you are already running a web-server on your machine, you will need to set up a reverse-proxy. + +#### Running BookWyrm Behind a Reverse-Proxy + +If you are running another web-server on your machine, you should have it handle proxying web requests to BookWyrm. + +The default BookWyrm configuration already has an nginx server that proxies requests to the django app that handles SSL and directly serves static files. +The static files are stored in a Docker volume that several BookWyrm services access, so it is not recommended to remove this server completely. + +To run BookWyrm behind a reverse-proxy, make the following changes: + - In `nginx/default.conf`: + - Comment out the two default servers + - Uncomment the server labeled Reverse-Proxy server + - Replace `your-domain.com` with your domain name + - In `docker-compose.yml`: + - In `services` -> `nginx` -> `ports`, comment out the default ports and add `- 8001:8001` + - In `services` -> `nginx` -> `volumes`, comment out the two volumes that begin `./certbot/` + - In `services`, comment out the `certbot` service + +At this point, you can follow, the [setup](#server-setup) instructions as listed. +Once docker is running, you can access your BookWyrm instance at `http://localhost:8001` (**NOTE:** your server is not accessible over `https`). + +Steps for setting up a reverse-proxy are server dependent. + +##### Nginx + +Before you can set up nginx, you will need to locate your nginx configuration directory, which is dependent on your platform and how you installed nginx. +See [nginx's guide](http://nginx.org/en/docs/beginners_guide.html) for details. + +To set up your server: + - In you `nginx.conf` file, ensure that `include servers/*;` isn't commented out. + - In your nginx `servers` directory, create a new file named after your domain containing the following information: + ```nginx + server { + server_name your-domain.com www.your-domain.com; + + location / { + proxy_pass http://localhost:8000; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + } + + location /images/ { + proxy_pass http://localhost:8001; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + } + + location /static/ { + proxy_pass http://localhost:8001; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + } + + listen [::]:80 ssl; + listen 80 ssl; + } + ``` + - run `sudo certbot run --nginx --email YOUR_EMAIL -d your-domain.com -d www.your-domain.com` + - restart nginx + +If everything worked correctly, your BookWyrm instance should now be externally accessible. \ No newline at end of file diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index fdfbb1f06..35b786f71 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -1,4 +1,4 @@ -''' bring activitypub functions into the namespace ''' +""" bring activitypub functions into the namespace """ import inspect import sys @@ -6,7 +6,8 @@ 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 .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 BookList, Shelf @@ -21,9 +22,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) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index c732fe1d3..791502d01 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -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,46 +8,52 @@ 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" try: - activity_type = activity_json['type'] + activity_type = activity_json["type"] serializer = activity_objects[activity_type] except KeyError as e: raise ActivitySerializerError(e) @@ -57,25 +63,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 +90,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) + ): raise ActivitySerializerError() # check for an existing instance @@ -142,8 +151,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 +172,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 +186,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,43 +212,45 @@ 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 + """ 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 @@ -249,13 +258,13 @@ def resolve_remote_id(remote_id, model=None, refresh=False, save=True): # 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')) + model = get_model_from_type(data.get("type")) # check for existing items with shared unique identifiers result = model.find_existing(data) diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 8c32be967..7e552b0a8 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -1,70 +1,75 @@ -''' 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 + @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 = '' + 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 = None - type: str = 'Book' + 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 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" diff --git a/bookwyrm/activitypub/image.py b/bookwyrm/activitypub/image.py index 569f83c5d..931de977b 100644 --- a/bookwyrm/activitypub/image.py +++ b/bookwyrm/activitypub/image.py @@ -1,11 +1,13 @@ -''' 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 ''' + """ image block """ + url: str - name: str = '' - type: str = 'Image' - id: str = '' + name: str = "" + type: str = "Document" + id: str = None diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index 705d6eede..a739eafa1 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -1,4 +1,4 @@ -''' 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 @@ -6,64 +6,81 @@ from django.apps import apps from .base_activity import ActivityObject, Link from .image import Image + @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: []) 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" diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index 14b35f3cf..6da608322 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -1,4 +1,4 @@ -''' defines activitypub collections (lists) ''' +""" defines activitypub collections (lists) """ from dataclasses import dataclass, field from typing import List @@ -7,38 +7,46 @@ 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" diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index 7e7d027eb..f1298b927 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -1,4 +1,4 @@ -''' actor serializer ''' +""" actor serializer """ from dataclasses import dataclass, field from typing import Dict @@ -8,25 +8,27 @@ 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 + 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' + type: str = "Person" diff --git a/bookwyrm/activitypub/response.py b/bookwyrm/activitypub/response.py index 8f3c050bb..07f39c7e1 100644 --- a/bookwyrm/activitypub/response.py +++ b/bookwyrm/activitypub/response.py @@ -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) diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 1236338b2..d684171e9 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -1,4 +1,4 @@ -''' undo wrapper activity ''' +""" undo wrapper activity """ from dataclasses import dataclass from typing import List from django.apps import apps @@ -9,160 +9,191 @@ from .book import Edition @dataclass(init=False) class Verb(ActivityObject): - ''' generic fields for activities - maybe an unecessary level of - abstraction but w/e ''' + """generic fields for activities - maybe an unecessary level of + abstraction but w/e""" + actor: str object: ActivityObject def action(self): - ''' usually we just want to save, this can be overridden as needed ''' + """ usually we just want to save, this can be overridden as needed """ self.object.to_model() @dataclass(init=False) class Create(Verb): - ''' Create activity ''' + """ Create activity """ + to: List cc: List signature: Signature = None - type: str = 'Create' + type: str = "Create" @dataclass(init=False) class Delete(Verb): - ''' Create activity ''' + """ Create activity """ + to: List cc: List - type: str = 'Delete' + type: str = "Delete" def action(self): - ''' find and delete the activity object ''' + """ find and delete the activity object """ obj = self.object.to_model(save=False, allow_create=False) obj.delete() - @dataclass(init=False) class Update(Verb): - ''' Update activity ''' + """ Update activity """ + to: List - type: str = 'Update' + type: str = "Update" def action(self): - ''' update a model instance from the dataclass ''' + """ update a model instance from the dataclass """ 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 ''' + """Add activity """ + target: str object: Edition - type: str = 'Add' + type: str = "Add" notes: str = None order: int = 0 approved: bool = True def action(self): - ''' add obj to collection ''' + """ 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 + # we want to get the related field that isn't the book, this is janky af sorry + model = [t for t in type(target)._meta.related_objects if t.name != "edition"][ + 0 + ].related_model self.to_model(model=model) @dataclass(init=False) class Remove(Verb): - '''Remove activity ''' + """Remove activity """ + target: ActivityObject - type: str = 'Remove' + type: str = "Remove" def action(self): - ''' find and remove the activity object ''' - obj = self.object.to_model(save=False, allow_create=False) + """ find and remove the activity object """ + target = resolve_remote_id(self.target, refresh=False) + model = [t for t in type(target)._meta.related_objects if t.name != "edition"][ + 0 + ].related_model + obj = self.to_model(model=model, save=False, allow_create=False) obj.delete() @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 """ + object: str - type: str = 'Announce' + type: str = "Announce" def action(self): - ''' boost ''' + """ boost """ self.to_model() diff --git a/bookwyrm/admin.py b/bookwyrm/admin.py index 45af81d99..efe5e9d72 100644 --- a/bookwyrm/admin.py +++ b/bookwyrm/admin.py @@ -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 diff --git a/bookwyrm/connectors/__init__.py b/bookwyrm/connectors/__init__.py index cfafd2868..689f27018 100644 --- a/bookwyrm/connectors/__init__.py +++ b/bookwyrm/connectors/__init__.py @@ -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 diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index e6372438e..00b5c5c9e 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -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,40 +25,29 @@ class AbstractMinimalConnector(ABC): # the things in the connector model to copy over self_fields = [ - 'base_url', - 'books_url', - 'covers_url', - 'search_url', - 'isbn_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]: @@ -63,74 +55,64 @@ class AbstractMinimalConnector(ABC): return results def isbn_search(self, query): - ''' isbn search ''' + """ isbn search """ params = {} - resp = requests.get( - '%s%s' % (self.isbn_search_url, query), + data = get_data( + "%s%s" % (self.isbn_search_url, query), params=params, - headers={ - 'Accept': 'application/json; charset=utf-8', - 'User-Agent': settings.USER_AGENT, - }, ) - if not resp.ok: - resp.raise_for_status() - try: - data = resp.json() - except ValueError as e: - logger.exception(e) - raise ConnectorException('Unable to parse json response', e) results = [] - for doc in self.parse_isbn_search_data(data): + # 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 ''' + """ 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 ''' + """ 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 @@ -154,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 @@ -168,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 @@ -189,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 @@ -203,48 +183,48 @@ class AbstractConnector(AbstractMinimalConnector): # 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 """ 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() @@ -260,12 +240,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: @@ -278,27 +258,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 "".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 @@ -307,11 +291,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 diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index 96b72f267..f7869d55c 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -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,13 +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): - search_result['connector'] = self - return SearchResult(**search_result) - + return self.format_search_result(search_result) diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index 053e1f9ef..3ed25cebb 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -1,4 +1,4 @@ -''' interface with whatever connectors the app has ''' +""" interface with whatever connectors the app has """ import importlib import re from urllib.parse import urlparse @@ -10,24 +10,24 @@ from bookwyrm.tasks import app 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 """ results = [] # Have we got a ISBN ? - isbn = re.sub('[\W_]', '', query) - maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 + 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) + dedup_slug = lambda r: "%s/%s/%s" % (r.title, r.author, r.year) result_index = set() for connector in get_connectors(): result_set = None if maybe_isbn: # Search on ISBN - if not connector.isbn_search_url or connector.isbn_search_url == '': + if not connector.isbn_search_url or connector.isbn_search_url == "": result_set = [] else: try: @@ -36,38 +36,39 @@ def search(query, min_confidence=0.1): pass # if no isbn search or results, we fallback to generic search - if result_set == None or result_set == []: + if result_set in (None, []): try: result_set = connector.search(query, min_confidence=min_confidence) except (HTTPError, ConnectorException): continue - result_set = [r for r in result_set \ - if dedup_slug(r) not in result_index] + 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 ''' + """ 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: @@ -76,29 +77,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 author'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) @@ -106,7 +107,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) @@ -114,8 +115,8 @@ 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) diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index 8d227eef1..9be0266cd 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -1,4 +1,4 @@ -''' openlibrary data connector ''' +""" openlibrary data connector """ import re from bookwyrm import models @@ -9,148 +9,139 @@ 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) + author_id = author_blob["key"] + url = "%s%s" % (self.base_url, author_id) yield self.get_or_create_author(url) - - 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] + 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'), + title=search_result.get("title"), key=key, - author=', '.join(author_names), + author=", ".join(author_names), connector=self, - year=search_result.get('publish_date'), + 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 @@ -164,7 +155,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 @@ -172,62 +163,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] diff --git a/bookwyrm/connectors/openlibrary_languages.py b/bookwyrm/connectors/openlibrary_languages.py index b687f8b97..2520d1ea1 100644 --- a/bookwyrm/connectors/openlibrary_languages.py +++ b/bookwyrm/connectors/openlibrary_languages.py @@ -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", } diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index b3a4d6f9f..500ffd74f 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -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 @@ -34,19 +35,18 @@ class Connector(AbstractConnector): return search_results def isbn_search(self, query, raw=False): - ''' search your local database ''' + """ search your local database """ if not query: return [] - filters = [{f: query} for f in ['isbn_10', 'isbn_13']] + 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 + results = results.filter(parent_work__default_edition__id=F("id")) or results search_results = [] for result in results: @@ -58,32 +58,21 @@ class Connector(AbstractConnector): 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 SearchResult( - title=search_result.title, - key=search_result.remote_id, - author=search_result.author_text, - year=search_result.published_date.year if \ - search_result.published_date else None, - connector=self, - confidence=search_result.rank if \ - hasattr(search_result, 'rank') else 1, - ) - + return self.format_search_result(search_result) def is_work_data(self, data): pass @@ -98,11 +87,11 @@ class Connector(AbstractConnector): return None def parse_isbn_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 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): @@ -110,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: diff --git a/bookwyrm/connectors/settings.py b/bookwyrm/connectors/settings.py index e04aedeff..f1674cf7c 100644 --- a/bookwyrm/connectors/settings.py +++ b/bookwyrm/connectors/settings.py @@ -1,3 +1,3 @@ -''' settings book data connectors ''' +""" settings book data connectors """ -CONNECTORS = ['openlibrary', 'self_connector', 'bookwyrm_connector'] +CONNECTORS = ["openlibrary", "self_connector", "bookwyrm_connector"] diff --git a/bookwyrm/context_processors.py b/bookwyrm/context_processors.py index a1471ac48..8f79a6529 100644 --- a/bookwyrm/context_processors.py +++ b/bookwyrm/context_processors.py @@ -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()} diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 2319d4677..c7536876d 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -1,25 +1,27 @@ -''' send emails ''' +""" send emails """ from django.core.mail import send_mail from bookwyrm import models from bookwyrm.tasks import app + def password_reset_email(reset_code): - ''' generate a password reset email ''' + """ 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 + "Reset your password on %s" % site.name, + "Your password reset link: %s" % reset_code.link, ) + @app.task def send_email(recipient, subject, message): - ''' use a task to send the email ''' + """ use a task to send the email """ send_mail( subject, message, - None, # sender will be the config default + None, # sender will be the config default [recipient], - fail_silently=False + fail_silently=False, ) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index b920fc9c0..edf1d9e45 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -1,4 +1,4 @@ -''' using django model forms ''' +""" using django model forms """ import datetime from collections import defaultdict @@ -12,99 +12,116 @@ 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'] + fields = ["user", "book", "content", "content_warning", "sensitive", "privacy"] 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 EditUserForm(CustomForm): class Meta: model = models.User fields = [ - 'avatar', 'name', 'email', 'summary', 'manually_approves_followers' + "avatar", + "name", + "email", + "summary", + "manually_approves_followers", + "show_goal", ] help_texts = {f: None for f in fields} @@ -112,15 +129,15 @@ class EditUserForm(CustomForm): 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} @@ -128,80 +145,87 @@ 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 CreateInviteForm(CustomForm): class Meta: model = models.SiteInvite - exclude = ['code', 'user', 'times_used'] + exclude = ["code", "user", "times_used"] 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): @@ -213,4 +237,10 @@ 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"] diff --git a/bookwyrm/goodreads_import.py b/bookwyrm/goodreads_import.py index f5b84e179..fb4e8e0f1 100644 --- a/bookwyrm/goodreads_import.py +++ b/bookwyrm/goodreads_import.py @@ -1,13 +1,14 @@ -''' handle reading a csv from goodreads ''' +""" 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 +# 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' + service = "GoodReads" def parse_fields(self, data): - data.update({'import_source': self.service }) + data.update({"import_source": self.service}) # add missing 'Date Started' field - data.update({'Date Started': None }) + data.update({"Date Started": None}) return data diff --git a/bookwyrm/importer.py b/bookwyrm/importer.py index a12884007..2fbb3430f 100644 --- a/bookwyrm/importer.py +++ b/bookwyrm/importer.py @@ -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,48 @@ from bookwyrm.tasks import app logger = logging.getLogger(__name__) + class Importer: - service = 'Unknown' - delimiter = ',' - encoding = 'UTF-8' - mandatory_fields = ['Title', 'Author'] + 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): ImportItem(job=job, index=index, data=data).save() def parse_fields(self, entry): - entry.update({'import_source': self.service }) - return entry + 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 +57,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 +73,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 +85,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 diff --git a/bookwyrm/librarything_import.py b/bookwyrm/librarything_import.py index 0584daad9..b3dd9d56b 100644 --- a/bookwyrm/librarything_import.py +++ b/bookwyrm/librarything_import.py @@ -1,4 +1,4 @@ -''' handle reading a csv from librarything ''' +""" handle reading a csv from librarything """ import csv import re import math @@ -9,34 +9,34 @@ from bookwyrm.importer import Importer class LibrarythingImporter(Importer): - service = 'LibraryThing' - delimiter = '\t' - encoding = 'ISO-8859-1' + service = "LibraryThing" + delimiter = "\t" + encoding = "ISO-8859-1" # mandatory_fields : fields matching the book title and author - mandatory_fields = ['Title', 'Primary 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'])) + 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["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" + 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" + data["Exclusive Shelf"] = "to-read" return data diff --git a/bookwyrm/management/commands/deduplicate_book_data.py b/bookwyrm/management/commands/deduplicate_book_data.py index 044b2a986..edd91a717 100644 --- a/bookwyrm/management/commands/deduplicate_book_data.py +++ b/bookwyrm/management/commands/deduplicate_book_data.py @@ -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) diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index 5759abfcc..d6101c877 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -5,51 +5,63 @@ from django.contrib.contenttypes.models import ContentType from bookwyrm.models import Connector, SiteSettings, User from bookwyrm.settings import DOMAIN + def init_groups(): - groups = ['admin', 'moderator', 'editor'] + 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,48 @@ 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, - isbn_search_url='https://%s/isbn/' % 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=', - isbn_search_url='https://bookwyrm.social/isbn/', + 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=', - isbn_search_url='https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:', + 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_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() diff --git a/bookwyrm/management/commands/remove_editions.py b/bookwyrm/management/commands/remove_editions.py index c5153f44b..6829c6d10 100644 --- a/bookwyrm/management/commands/remove_editions.py +++ b/bookwyrm/management/commands/remove_editions.py @@ -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() diff --git a/bookwyrm/migrations/0001_initial.py b/bookwyrm/migrations/0001_initial.py index 347057e1d..a405b956f 100644 --- a/bookwyrm/migrations/0001_initial.py +++ b/bookwyrm/migrations/0001_initial.py @@ -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",), ), ] diff --git a/bookwyrm/migrations/0002_auto_20200219_0816.py b/bookwyrm/migrations/0002_auto_20200219_0816.py index 9cb5b726d..07daad935 100644 --- a/bookwyrm/migrations/0002_auto_20200219_0816.py +++ b/bookwyrm/migrations/0002_auto_20200219_0816.py @@ -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", + ), ), ] diff --git a/bookwyrm/migrations/0003_auto_20200221_0131.py b/bookwyrm/migrations/0003_auto_20200221_0131.py index e53f042b4..e3e164140 100644 --- a/bookwyrm/migrations/0003_auto_20200221_0131.py +++ b/bookwyrm/migrations/0003_auto_20200221_0131.py @@ -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), ), ] diff --git a/bookwyrm/migrations/0004_tag.py b/bookwyrm/migrations/0004_tag.py index 209550008..b6210070c 100644 --- a/bookwyrm/migrations/0004_tag.py +++ b/bookwyrm/migrations/0004_tag.py @@ -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")}, }, ), ] diff --git a/bookwyrm/migrations/0005_auto_20200221_1645.py b/bookwyrm/migrations/0005_auto_20200221_1645.py index dbd87e924..449ce041e 100644 --- a/bookwyrm/migrations/0005_auto_20200221_1645.py +++ b/bookwyrm/migrations/0005_auto_20200221_1645.py @@ -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), diff --git a/bookwyrm/migrations/0006_auto_20200221_1702_squashed_0064_merge_20201101_1913.py b/bookwyrm/migrations/0006_auto_20200221_1702_squashed_0064_merge_20201101_1913.py index 6a149ab59..c06fa40a0 100644 --- a/bookwyrm/migrations/0006_auto_20200221_1702_squashed_0064_merge_20201101_1913.py +++ b/bookwyrm/migrations/0006_auto_20200221_1702_squashed_0064_merge_20201101_1913.py @@ -16,1056 +16,1647 @@ import uuid class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0005_auto_20200221_1645'), + ("bookwyrm", "0005_auto_20200221_1645"), ] operations = [ migrations.AlterField( - model_name='tag', - name='identifier', + model_name="tag", + name="identifier", field=models.CharField(max_length=100), ), migrations.AddConstraint( - model_name='userrelationship', - constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='followers_unique'), + model_name="userrelationship", + constraint=models.UniqueConstraint( + fields=("user_subject", "user_object"), name="followers_unique" + ), ), migrations.RemoveField( - model_name='user', - name='followers', + model_name="user", + name="followers", ), migrations.AddField( - model_name='status', - name='published_date', + model_name="status", + name="published_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.CreateModel( - name='Edition', + name="Edition", fields=[ - ('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Book')), - ('isbn', models.CharField(max_length=255, null=True, unique=True)), - ('oclc_number', models.CharField(max_length=255, null=True, unique=True)), - ('pages', models.IntegerField(null=True)), + ( + "book_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Book", + ), + ), + ("isbn", models.CharField(max_length=255, null=True, unique=True)), + ( + "oclc_number", + models.CharField(max_length=255, null=True, unique=True), + ), + ("pages", models.IntegerField(null=True)), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.book',), + bases=("bookwyrm.book",), ), migrations.CreateModel( - name='Work', + name="Work", fields=[ - ('book_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Book')), - ('lccn', models.CharField(max_length=255, null=True, unique=True)), + ( + "book_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Book", + ), + ), + ("lccn", models.CharField(max_length=255, null=True, unique=True)), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.book',), + bases=("bookwyrm.book",), ), migrations.RemoveField( - model_name='author', - name='data', + model_name="author", + name="data", ), migrations.RemoveField( - model_name='book', - name='added_by', + model_name="book", + name="added_by", ), migrations.RemoveField( - model_name='book', - name='data', + model_name="book", + name="data", ), migrations.AddField( - model_name='author', - name='bio', + model_name="author", + name="bio", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='author', - name='born', + model_name="author", + name="born", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='author', - name='died', + model_name="author", + name="died", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='author', - name='first_name', + model_name="author", + name="first_name", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='author', - name='last_name', + model_name="author", + name="last_name", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='author', - name='name', - field=models.CharField(default='Unknown', max_length=255), + model_name="author", + name="name", + field=models.CharField(default="Unknown", max_length=255), preserve_default=False, ), migrations.AddField( - model_name='author', - name='wikipedia_link', + model_name="author", + name="wikipedia_link", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='description', + model_name="book", + name="description", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='book', - name='first_published_date', + model_name="book", + name="first_published_date", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='book', - name='language', + model_name="book", + name="language", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='last_sync_date', + model_name="book", + name="last_sync_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='book', - name='librarything_key', + model_name="book", + name="librarything_key", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.AddField( - model_name='book', - name='local_edits', + model_name="book", + name="local_edits", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='book', - name='local_key', + model_name="book", + name="local_key", field=models.CharField(default=uuid.uuid4, max_length=255, unique=True), ), migrations.AddField( - model_name='book', - name='misc_identifiers', + model_name="book", + name="misc_identifiers", field=JSONField(null=True), ), migrations.AddField( - model_name='book', - name='origin', + model_name="book", + name="origin", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.AddField( - model_name='book', - name='published_date', + model_name="book", + name="published_date", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='book', - name='series', + model_name="book", + name="series", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='series_number', + model_name="book", + name="series_number", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='sort_title', + model_name="book", + name="sort_title", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='book', - name='subtitle', + model_name="book", + name="subtitle", field=models.TextField(blank=True, null=True), ), migrations.AddField( - model_name='book', - name='sync', + model_name="book", + name="sync", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='book', - name='title', - field=models.CharField(default='Unknown', max_length=255), + model_name="book", + name="title", + field=models.CharField(default="Unknown", max_length=255), preserve_default=False, ), migrations.AlterField( - model_name='author', - name='openlibrary_key', + model_name="author", + name="openlibrary_key", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.AlterField( - model_name='book', - name='openlibrary_key', + model_name="book", + name="openlibrary_key", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.AddField( - model_name='book', - name='parent_work', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Work'), + model_name="book", + name="parent_work", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Work", + ), ), migrations.CreateModel( - name='Notification', + name="Notification", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('read', models.BooleanField(default=False)), - ('notification_type', models.CharField(max_length=255)), - ('related_book', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), - ('related_status', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Status')), - ('related_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='related_user', to=settings.AUTH_USER_MODEL)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ("read", models.BooleanField(default=False)), + ("notification_type", models.CharField(max_length=255)), + ( + "related_book", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Book", + ), + ), + ( + "related_status", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Status", + ), + ), + ( + "related_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="related_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.AddField( - 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.AddField( - model_name='user', - name='manually_approves_followers', + model_name="user", + name="manually_approves_followers", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='status', - name='remote_id', + model_name="status", + name="remote_id", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.CreateModel( - name='UserBlocks', + name="UserBlocks", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('relationship_id', models.CharField(max_length=100)), - ('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_object', to=settings.AUTH_USER_MODEL)), - ('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userblocks_user_subject', 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)), + ("relationship_id", models.CharField(max_length=100)), + ( + "user_object", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userblocks_user_object", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_subject", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userblocks_user_subject", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='UserFollowRequest', + name="UserFollowRequest", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('relationship_id', models.CharField(max_length=100)), - ('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_object', to=settings.AUTH_USER_MODEL)), - ('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollowrequest_user_subject', 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)), + ("relationship_id", models.CharField(max_length=100)), + ( + "user_object", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollowrequest_user_object", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_subject", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollowrequest_user_subject", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='UserFollows', + name="UserFollows", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('relationship_id', models.CharField(max_length=100)), - ('user_object', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_object', to=settings.AUTH_USER_MODEL)), - ('user_subject', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='userfollows_user_subject', 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)), + ("relationship_id", models.CharField(max_length=100)), + ( + "user_object", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollows_user_object", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_subject", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="userfollows_user_subject", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.DeleteModel( - name='UserRelationship', + name="UserRelationship", ), migrations.AddField( - model_name='user', - name='blocks', - field=models.ManyToManyField(related_name='blocked_by', through='bookwyrm.UserBlocks', to=settings.AUTH_USER_MODEL), + model_name="user", + name="blocks", + field=models.ManyToManyField( + related_name="blocked_by", + through="bookwyrm.UserBlocks", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='user', - name='follow_requests', - field=models.ManyToManyField(related_name='follower_requests', through='bookwyrm.UserFollowRequest', to=settings.AUTH_USER_MODEL), + model_name="user", + name="follow_requests", + field=models.ManyToManyField( + related_name="follower_requests", + through="bookwyrm.UserFollowRequest", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='user', - name='following', - field=models.ManyToManyField(related_name='followers', through='bookwyrm.UserFollows', to=settings.AUTH_USER_MODEL), + model_name="user", + name="following", + field=models.ManyToManyField( + related_name="followers", + through="bookwyrm.UserFollows", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddConstraint( - model_name='userfollows', - constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userfollows_unique'), + model_name="userfollows", + constraint=models.UniqueConstraint( + fields=("user_subject", "user_object"), name="userfollows_unique" + ), ), migrations.AddConstraint( - model_name='userfollowrequest', - constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userfollowrequest_unique'), + model_name="userfollowrequest", + constraint=models.UniqueConstraint( + fields=("user_subject", "user_object"), name="userfollowrequest_unique" + ), ), migrations.AddConstraint( - model_name='userblocks', - constraint=models.UniqueConstraint(fields=('user_subject', 'user_object'), name='userblocks_unique'), + model_name="userblocks", + constraint=models.UniqueConstraint( + fields=("user_subject", "user_object"), name="userblocks_unique" + ), ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + ] + ), + name="notification_type_valid", + ), ), migrations.AddConstraint( - model_name='userblocks', - constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userblocks_no_self'), + model_name="userblocks", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, + user_subject=django.db.models.expressions.F("user_object"), + ), + name="userblocks_no_self", + ), ), migrations.AddConstraint( - model_name='userfollowrequest', - constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userfollowrequest_no_self'), + model_name="userfollowrequest", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, + user_subject=django.db.models.expressions.F("user_object"), + ), + name="userfollowrequest_no_self", + ), ), migrations.AddConstraint( - model_name='userfollows', - constraint=models.CheckConstraint(check=models.Q(_negated=True, user_subject=django.db.models.expressions.F('user_object')), name='userfollows_no_self'), + model_name="userfollows", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, + user_subject=django.db.models.expressions.F("user_object"), + ), + name="userfollows_no_self", + ), ), migrations.AddField( - model_name='favorite', - name='remote_id', + model_name="favorite", + name="remote_id", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.CreateModel( - name='Comment', + name="Comment", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), - ('name', models.CharField(max_length=255)), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book" + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), migrations.CreateModel( - name='Connector', + name="Connector", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('identifier', models.CharField(max_length=255, unique=True)), - ('connector_file', models.CharField(choices=[('openlibrary', 'Openlibrary'), ('bookwyrm', 'BookWyrm')], default='openlibrary', max_length=255)), - ('is_self', models.BooleanField(default=False)), - ('api_key', models.CharField(max_length=255, null=True)), - ('base_url', models.CharField(max_length=255)), - ('covers_url', models.CharField(max_length=255)), - ('search_url', models.CharField(max_length=255, null=True)), - ('key_name', models.CharField(max_length=255)), - ('politeness_delay', models.IntegerField(null=True)), - ('max_query_count', models.IntegerField(null=True)), - ('query_count', models.IntegerField(default=0)), - ('query_count_expiry', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ("identifier", models.CharField(max_length=255, unique=True)), + ( + "connector_file", + models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("bookwyrm", "BookWyrm"), + ], + default="openlibrary", + max_length=255, + ), + ), + ("is_self", models.BooleanField(default=False)), + ("api_key", models.CharField(max_length=255, null=True)), + ("base_url", models.CharField(max_length=255)), + ("covers_url", models.CharField(max_length=255)), + ("search_url", models.CharField(max_length=255, null=True)), + ("key_name", models.CharField(max_length=255)), + ("politeness_delay", models.IntegerField(null=True)), + ("max_query_count", models.IntegerField(null=True)), + ("query_count", models.IntegerField(default=0)), + ("query_count_expiry", models.DateTimeField(auto_now_add=True)), ], ), migrations.RenameField( - model_name='book', - old_name='local_key', - new_name='fedireads_key', + model_name="book", + old_name="local_key", + new_name="fedireads_key", ), migrations.RenameField( - model_name='book', - old_name='origin', - new_name='source_url', + model_name="book", + old_name="origin", + new_name="source_url", ), migrations.RemoveField( - model_name='book', - name='local_edits', + model_name="book", + name="local_edits", ), migrations.AddConstraint( - model_name='connector', - constraint=models.CheckConstraint(check=models.Q(connector_file__in=bookwyrm.models.connector.ConnectorFiles), name='connector_file_valid'), + model_name="connector", + constraint=models.CheckConstraint( + check=models.Q( + connector_file__in=bookwyrm.models.connector.ConnectorFiles + ), + name="connector_file_valid", + ), ), migrations.AddField( - model_name='book', - name='connector', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Connector'), + model_name="book", + name="connector", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Connector", + ), ), migrations.AddField( - model_name='book', - name='subject_places', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="book", + name="subject_places", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AddField( - model_name='book', - name='subjects', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="book", + name="subjects", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AddField( - model_name='edition', - name='publishers', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="edition", + name="publishers", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('fedireads_connector', 'Fedireads Connector')], default='openlibrary', max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("fedireads_connector", "Fedireads Connector"), + ], + default="openlibrary", + max_length=255, + ), ), migrations.RemoveField( - model_name='connector', - name='is_self', + model_name="connector", + name="is_self", ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('fedireads_connector', 'Fedireads Connector')], default='openlibrary', max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("self_connector", "Self Connector"), + ("fedireads_connector", "Fedireads Connector"), + ], + default="openlibrary", + max_length=255, + ), ), migrations.AddField( - model_name='book', - name='sync_cover', + model_name="book", + name="sync_cover", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='author', - name='born', + model_name="author", + name="born", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='author', - name='died', + model_name="author", + name="died", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='author', - name='fedireads_key', + model_name="author", + name="fedireads_key", field=models.CharField(default=uuid.uuid4, max_length=255, unique=True), ), migrations.AlterField( - model_name='author', - name='first_name', + model_name="author", + name="first_name", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='author', - name='last_name', + model_name="author", + name="last_name", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='author', - name='openlibrary_key', + model_name="author", + name="openlibrary_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='first_published_date', + model_name="book", + name="first_published_date", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='book', - name='goodreads_key', + model_name="book", + name="goodreads_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='language', + model_name="book", + name="language", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='librarything_key', + model_name="book", + name="librarything_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='openlibrary_key', + model_name="book", + name="openlibrary_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='published_date', + model_name="book", + name="published_date", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='book', - name='sort_title', + model_name="book", + name="sort_title", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='subtitle', + model_name="book", + name="subtitle", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='edition', - name='isbn', + model_name="edition", + name="isbn", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='edition', - name='oclc_number', + model_name="edition", + name="oclc_number", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='edition', - name='pages', + model_name="edition", + name="pages", field=models.IntegerField(blank=True, null=True), ), migrations.AddField( - model_name='edition', - name='physical_format', + model_name="edition", + name="physical_format", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='work', - name='lccn', + model_name="work", + name="lccn", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='federatedserver', - name='application_version', + model_name="federatedserver", + name="application_version", field=models.CharField(max_length=255, null=True), ), migrations.AlterField( - model_name='book', - name='last_sync_date', + model_name="book", + name="last_sync_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AlterField( - model_name='status', - name='published_date', + model_name="status", + name="published_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.CreateModel( - name='Boost', + name="Boost", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + ] + ), + name="notification_type_valid", + ), ), migrations.AddField( - model_name='boost', - name='boosted_status', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='boosters', to='bookwyrm.Status'), + model_name="boost", + name="boosted_status", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="boosters", + to="bookwyrm.Status", + ), ), migrations.RemoveField( - model_name='book', - name='language', + model_name="book", + name="language", ), migrations.RemoveField( - model_name='book', - name='parent_work', + model_name="book", + name="parent_work", ), migrations.RemoveField( - model_name='book', - name='shelves', + model_name="book", + name="shelves", ), migrations.AddField( - model_name='book', - name='languages', - field=ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None), + model_name="book", + name="languages", + field=ArrayField( + base_field=models.CharField(max_length=255), + blank=True, + default=list, + size=None, + ), ), migrations.AddField( - model_name='edition', - name='default', + model_name="edition", + name="default", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='edition', - name='parent_work', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Work'), + model_name="edition", + name="parent_work", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Work", + ), ), migrations.AddField( - model_name='edition', - name='shelves', - field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Shelf'), + model_name="edition", + name="shelves", + field=models.ManyToManyField( + through="bookwyrm.ShelfBook", to="bookwyrm.Shelf" + ), ), migrations.AlterField( - model_name='comment', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="comment", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='notification', - name='related_book', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="notification", + name="related_book", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Edition", + ), ), migrations.AlterField( - model_name='review', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="review", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='shelf', - name='books', - field=models.ManyToManyField(through='bookwyrm.ShelfBook', to='bookwyrm.Edition'), + model_name="shelf", + name="books", + field=models.ManyToManyField( + through="bookwyrm.ShelfBook", to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='shelfbook', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="shelfbook", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='status', - name='mention_books', - field=models.ManyToManyField(related_name='mention_book', to='bookwyrm.Edition'), + model_name="status", + name="mention_books", + field=models.ManyToManyField( + related_name="mention_book", to="bookwyrm.Edition" + ), ), migrations.AlterField( - model_name='tag', - name='book', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition'), + model_name="tag", + name="book", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Edition" + ), ), migrations.RemoveField( - model_name='comment', - name='name', + model_name="comment", + name="name", ), migrations.AlterField( - model_name='review', - name='rating', - field=models.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), + model_name="review", + name="rating", + field=models.IntegerField( + blank=True, + default=None, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(5), + ], + ), ), migrations.AlterField( - model_name='review', - name='name', + model_name="review", + name="name", field=models.CharField(max_length=255, null=True), ), migrations.CreateModel( - name='Quotation', + name="Quotation", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), - ('quote', models.TextField()), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), + ("quote", models.TextField()), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Edition", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), migrations.CreateModel( - name='ReadThrough', + name="ReadThrough", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('pages_read', models.IntegerField(blank=True, null=True)), - ('start_date', models.DateTimeField(blank=True, null=True)), - ('finish_date', models.DateTimeField(blank=True, null=True)), - ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Book')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ("pages_read", models.IntegerField(blank=True, null=True)), + ("start_date", models.DateTimeField(blank=True, null=True)), + ("finish_date", models.DateTimeField(blank=True, null=True)), + ( + "book", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.Book" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='ImportItem', + name="ImportItem", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('data', JSONField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("data", JSONField()), ], ), migrations.CreateModel( - name='ImportJob', + name="ImportJob", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(default=django.utils.timezone.now)), - ('task_id', models.CharField(max_length=100, null=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("task_id", models.CharField(max_length=100, null=True)), ], ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT_RESULT', 'Import Result')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT_RESULT", "Import Result"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT_RESULT']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT_RESULT", + ] + ), + name="notification_type_valid", + ), ), migrations.AddField( - model_name='importjob', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="importjob", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='importitem', - name='book', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='bookwyrm.Book'), + model_name="importitem", + name="book", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="bookwyrm.Book", + ), ), migrations.AddField( - model_name='importitem', - name='job', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='bookwyrm.ImportJob'), + model_name="importitem", + name="job", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="items", + to="bookwyrm.ImportJob", + ), ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AddField( - model_name='importitem', - name='fail_reason', + model_name="importitem", + name="fail_reason", field=models.TextField(null=True), ), migrations.AddField( - model_name='importitem', - name='index', + model_name="importitem", + name="index", field=models.IntegerField(default=1), preserve_default=False, ), migrations.AddField( - model_name='notification', - name='related_import', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.ImportJob'), + model_name="notification", + name="related_import", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.ImportJob", + ), ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT", + ] + ), + name="notification_type_valid", + ), ), migrations.RenameField( - model_name='edition', - old_name='isbn', - new_name='isbn_13', + model_name="edition", + old_name="isbn", + new_name="isbn_13", ), migrations.AddField( - model_name='book', - name='author_text', + model_name="book", + name="author_text", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='edition', - name='asin', + model_name="edition", + name="asin", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='edition', - name='isbn_10', + model_name="edition", + name="isbn_10", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='connector', - name='books_url', - field=models.CharField(default='https://openlibrary.org', max_length=255), + model_name="connector", + name="books_url", + field=models.CharField(default="https://openlibrary.org", max_length=255), preserve_default=False, ), migrations.AddField( - model_name='connector', - name='local', + model_name="connector", + name="local", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='connector', - name='name', + model_name="connector", + name="name", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='connector', - name='priority', + model_name="connector", + name="priority", field=models.IntegerField(default=2), ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('fedireads_connector', 'Fedireads Connector')], max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("self_connector", "Self Connector"), + ("fedireads_connector", "Fedireads Connector"), + ], + max_length=255, + ), ), migrations.RemoveField( - model_name='author', - name='fedireads_key', + model_name="author", + name="fedireads_key", ), migrations.RemoveField( - model_name='book', - name='fedireads_key', + model_name="book", + name="fedireads_key", ), migrations.RemoveField( - model_name='book', - name='source_url', + model_name="book", + name="source_url", ), migrations.AddField( - model_name='author', - name='last_sync_date', + model_name="author", + name="last_sync_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='author', - name='sync', + model_name="author", + name="sync", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='book', - name='remote_id', + model_name="book", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='author', - name='remote_id', + model_name="author", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.RemoveField( - model_name='book', - name='misc_identifiers', + model_name="book", + name="misc_identifiers", ), migrations.RemoveField( - model_name='connector', - name='key_name', + model_name="connector", + name="key_name", ), migrations.RemoveField( - model_name='user', - name='actor', + model_name="user", + name="actor", ), migrations.AddField( - model_name='connector', - name='remote_id', + model_name="connector", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='federatedserver', - name='remote_id', + model_name="federatedserver", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='notification', - name='remote_id', + model_name="notification", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='readthrough', - name='remote_id', + model_name="readthrough", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='shelf', - name='remote_id', + model_name="shelf", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='shelfbook', - name='remote_id', + model_name="shelfbook", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='tag', - name='remote_id', + model_name="tag", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='userblocks', - name='remote_id', + model_name="userblocks", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='userfollowrequest', - name='remote_id', + model_name="userfollowrequest", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='userfollows', - name='remote_id', + model_name="userfollows", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AlterField( - model_name='favorite', - name='remote_id', + model_name="favorite", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AlterField( - model_name='status', - name='remote_id', + model_name="status", + name="remote_id", field=models.CharField(max_length=255, null=True), ), migrations.AddField( - model_name='user', - name='remote_id', + model_name="user", + name="remote_id", field=models.CharField(max_length=255, null=True, unique=True), ), migrations.CreateModel( - name='SiteInvite', + name="SiteInvite", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(default=bookwyrm.models.site.new_access_code, max_length=32)), - ('expiry', models.DateTimeField(blank=True, null=True)), - ('use_limit', models.IntegerField(blank=True, null=True)), - ('times_used', models.IntegerField(default=0)), - ('user', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + default=bookwyrm.models.site.new_access_code, max_length=32 + ), + ), + ("expiry", models.DateTimeField(blank=True, null=True)), + ("use_limit", models.IntegerField(blank=True, null=True)), + ("times_used", models.IntegerField(default=0)), + ( + "user", + models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.RemoveField( - model_name='status', - name='activity_type', + model_name="status", + name="activity_type", ), migrations.RemoveField( - model_name='status', - name='status_type', + model_name="status", + name="status_type", ), migrations.RenameField( - model_name='user', - old_name='fedireads_user', - new_name='bookwyrm_user', + model_name="user", + old_name="fedireads_user", + new_name="bookwyrm_user", ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('bookwyrm_connector', 'BookWyrm Connector')], max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("self_connector", "Self Connector"), + ("bookwyrm_connector", "BookWyrm Connector"), + ], + max_length=255, + ), ), migrations.AlterField( - model_name='connector', - name='connector_file', - field=models.CharField(choices=[('openlibrary', 'Openlibrary'), ('self_connector', 'Self Connector'), ('bookwyrm_connector', 'Bookwyrm Connector')], max_length=255), + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("self_connector", "Self Connector"), + ("bookwyrm_connector", "Bookwyrm Connector"), + ], + max_length=255, + ), ), migrations.CreateModel( - name='GeneratedStatus', + name="GeneratedStatus", fields=[ - ('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='bookwyrm.Status')), + ( + "status_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Status", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('bookwyrm.status',), + bases=("bookwyrm.status",), ), migrations.CreateModel( - name='PasswordReset', + name="PasswordReset", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(default=bookwyrm.models.site.new_access_code, max_length=32)), - ('expiry', models.DateTimeField(default=bookwyrm.models.site.get_passowrd_reset_expiry)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + default=bookwyrm.models.site.new_access_code, max_length=32 + ), + ), + ( + "expiry", + models.DateTimeField( + default=bookwyrm.models.site.get_passowrd_reset_expiry + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AlterField( - model_name='user', - name='email', + model_name="user", + name="email", field=models.EmailField(max_length=254, unique=True), ), migrations.CreateModel( - name='SiteSettings', + name="SiteSettings", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default='BookWyrm', max_length=100)), - ('instance_description', models.TextField(default='This instance has no description.')), - ('code_of_conduct', models.TextField(default='Add a code of conduct here.')), - ('allow_registration', models.BooleanField(default=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(default="BookWyrm", max_length=100)), + ( + "instance_description", + models.TextField(default="This instance has no description."), + ), + ( + "code_of_conduct", + models.TextField(default="Add a code of conduct here."), + ), + ("allow_registration", models.BooleanField(default=True)), ], ), migrations.AlterField( - model_name='user', - name='email', - field=models.EmailField(blank=True, max_length=254, verbose_name='email address'), + model_name="user", + name="email", + field=models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), ), migrations.AddField( - model_name='status', - name='deleted', + model_name="status", + name="deleted", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='status', - name='deleted_date', + model_name="status", + name="deleted_date", field=models.DateTimeField(), ), - django.contrib.postgres.operations.TrigramExtension( + django.contrib.postgres.operations.TrigramExtension(), + migrations.RemoveField( + model_name="userblocks", + name="relationship_id", ), migrations.RemoveField( - model_name='userblocks', - name='relationship_id', + model_name="userfollowrequest", + name="relationship_id", ), migrations.RemoveField( - model_name='userfollowrequest', - name='relationship_id', - ), - migrations.RemoveField( - model_name='userfollows', - name='relationship_id', + model_name="userfollows", + name="relationship_id", ), migrations.AlterField( - model_name='status', - name='deleted_date', + model_name="status", + name="deleted_date", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='status', - name='privacy', - field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="status", + name="privacy", + field=models.CharField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), migrations.AddField( - model_name='importjob', - name='include_reviews', + model_name="importjob", + name="include_reviews", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='importjob', - name='privacy', - field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + model_name="importjob", + name="privacy", + field=models.CharField( + choices=[ + ("public", "Public"), + ("unlisted", "Unlisted"), + ("followers", "Followers"), + ("direct", "Direct"), + ], + default="public", + max_length=255, + ), ), migrations.AlterField( - model_name='user', - name='federated_server', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.FederatedServer'), + model_name="user", + name="federated_server", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.FederatedServer", + ), ), migrations.RenameModel( - old_name='GeneratedStatus', - new_name='GeneratedNote', + old_name="GeneratedStatus", + new_name="GeneratedNote", ), migrations.AlterField( - model_name='connector', - name='api_key', + model_name="connector", + name="api_key", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='connector', - name='max_query_count', + model_name="connector", + name="max_query_count", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='connector', - name='name', + model_name="connector", + name="name", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AlterField( - model_name='connector', - name='politeness_delay', + model_name="connector", + name="politeness_delay", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='connector', - name='search_url', + model_name="connector", + name="search_url", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='user', - name='last_active_date', + model_name="user", + name="last_active_date", field=models.DateTimeField(auto_now=True), ), migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ], + max_length=255, + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "MENTION", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT", + ] + ), + name="notification_type_valid", + ), ), ] diff --git a/bookwyrm/migrations/0007_auto_20201103_0014.py b/bookwyrm/migrations/0007_auto_20201103_0014.py index bf0a12eb0..116c97a3e 100644 --- a/bookwyrm/migrations/0007_auto_20201103_0014.py +++ b/bookwyrm/migrations/0007_auto_20201103_0014.py @@ -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 + ), ), ] diff --git a/bookwyrm/migrations/0008_work_default_edition.py b/bookwyrm/migrations/0008_work_default_edition.py index da1f959e8..787e3776a 100644 --- a/bookwyrm/migrations/0008_work_default_edition.py +++ b/bookwyrm/migrations/0008_work_default_edition.py @@ -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", ), ] diff --git a/bookwyrm/migrations/0009_shelf_privacy.py b/bookwyrm/migrations/0009_shelf_privacy.py index 8232c2edc..635661045 100644 --- a/bookwyrm/migrations/0009_shelf_privacy.py +++ b/bookwyrm/migrations/0009_shelf_privacy.py @@ -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, + ), ), ] diff --git a/bookwyrm/migrations/0010_importjob_retry.py b/bookwyrm/migrations/0010_importjob_retry.py index 21296cc45..b3cc371bb 100644 --- a/bookwyrm/migrations/0010_importjob_retry.py +++ b/bookwyrm/migrations/0010_importjob_retry.py @@ -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), ), ] diff --git a/bookwyrm/migrations/0011_auto_20201113_1727.py b/bookwyrm/migrations/0011_auto_20201113_1727.py index 15e853a35..f4ea55c59 100644 --- a/bookwyrm/migrations/0011_auto_20201113_1727.py +++ b/bookwyrm/migrations/0011_auto_20201113_1727.py @@ -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), diff --git a/bookwyrm/migrations/0012_attachment.py b/bookwyrm/migrations/0012_attachment.py index 495538517..5188b463b 100644 --- a/bookwyrm/migrations/0012_attachment.py +++ b/bookwyrm/migrations/0012_attachment.py @@ -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, }, ), ] diff --git a/bookwyrm/migrations/0012_progressupdate.py b/bookwyrm/migrations/0012_progressupdate.py index 131419712..566556b7e 100644 --- a/bookwyrm/migrations/0012_progressupdate.py +++ b/bookwyrm/migrations/0012_progressupdate.py @@ -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, }, ), ] diff --git a/bookwyrm/migrations/0013_book_origin_id.py b/bookwyrm/migrations/0013_book_origin_id.py index 581a2406e..08cf7bee7 100644 --- a/bookwyrm/migrations/0013_book_origin_id.py +++ b/bookwyrm/migrations/0013_book_origin_id.py @@ -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), ), ] diff --git a/bookwyrm/migrations/0014_auto_20201128_0118.py b/bookwyrm/migrations/0014_auto_20201128_0118.py index babdd7805..2626b9652 100644 --- a/bookwyrm/migrations/0014_auto_20201128_0118.py +++ b/bookwyrm/migrations/0014_auto_20201128_0118.py @@ -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", ), ] diff --git a/bookwyrm/migrations/0014_merge_20201128_0007.py b/bookwyrm/migrations/0014_merge_20201128_0007.py index e811fa7ff..ce6bb5c03 100644 --- a/bookwyrm/migrations/0014_merge_20201128_0007.py +++ b/bookwyrm/migrations/0014_merge_20201128_0007.py @@ -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 = [] diff --git a/bookwyrm/migrations/0015_auto_20201128_0349.py b/bookwyrm/migrations/0015_auto_20201128_0349.py index 52b155186..f4454c5db 100644 --- a/bookwyrm/migrations/0015_auto_20201128_0349.py +++ b/bookwyrm/migrations/0015_auto_20201128_0349.py @@ -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", + ), ), ] diff --git a/bookwyrm/migrations/0015_auto_20201128_0734.py b/bookwyrm/migrations/0015_auto_20201128_0734.py index c6eb78150..efbad6109 100644 --- a/bookwyrm/migrations/0015_auto_20201128_0734.py +++ b/bookwyrm/migrations/0015_auto_20201128_0734.py @@ -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 + ), ), ] diff --git a/bookwyrm/migrations/0016_auto_20201129_0304.py b/bookwyrm/migrations/0016_auto_20201129_0304.py index 1e7159691..ef2cbe0f5 100644 --- a/bookwyrm/migrations/0016_auto_20201129_0304.py +++ b/bookwyrm/migrations/0016_auto_20201129_0304.py @@ -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")}, }, ), ] diff --git a/bookwyrm/migrations/0016_auto_20201211_2026.py b/bookwyrm/migrations/0016_auto_20201211_2026.py index 46b6140c3..3793f90ba 100644 --- a/bookwyrm/migrations/0016_auto_20201211_2026.py +++ b/bookwyrm/migrations/0016_auto_20201211_2026.py @@ -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), ), ] diff --git a/bookwyrm/migrations/0017_auto_20201130_1819.py b/bookwyrm/migrations/0017_auto_20201130_1819.py index 0775269b6..f6478e0a5 100644 --- a/bookwyrm/migrations/0017_auto_20201130_1819.py +++ b/bookwyrm/migrations/0017_auto_20201130_1819.py @@ -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), ] diff --git a/bookwyrm/migrations/0017_auto_20201212_0059.py b/bookwyrm/migrations/0017_auto_20201212_0059.py index c9e3fcf4e..34d27a1f9 100644 --- a/bookwyrm/migrations/0017_auto_20201212_0059.py +++ b/bookwyrm/migrations/0017_auto_20201212_0059.py @@ -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" + ), ), ] diff --git a/bookwyrm/migrations/0018_auto_20201130_1832.py b/bookwyrm/migrations/0018_auto_20201130_1832.py index 278446cf5..579b09f2f 100644 --- a/bookwyrm/migrations/0018_auto_20201130_1832.py +++ b/bookwyrm/migrations/0018_auto_20201130_1832.py @@ -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", ), ] diff --git a/bookwyrm/migrations/0019_auto_20201130_1939.py b/bookwyrm/migrations/0019_auto_20201130_1939.py index 11cf6a3b6..e5e7674a1 100644 --- a/bookwyrm/migrations/0019_auto_20201130_1939.py +++ b/bookwyrm/migrations/0019_auto_20201130_1939.py @@ -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=""), ), ] diff --git a/bookwyrm/migrations/0020_auto_20201208_0213.py b/bookwyrm/migrations/0020_auto_20201208_0213.py index 9c5345c75..79d9e73dd 100644 --- a/bookwyrm/migrations/0020_auto_20201208_0213.py +++ b/bookwyrm/migrations/0020_auto_20201208_0213.py @@ -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 + ), ), ] diff --git a/bookwyrm/migrations/0021_merge_20201212_1737.py b/bookwyrm/migrations/0021_merge_20201212_1737.py index 4ccf8c8cc..c6b48820d 100644 --- a/bookwyrm/migrations/0021_merge_20201212_1737.py +++ b/bookwyrm/migrations/0021_merge_20201212_1737.py @@ -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 = [] diff --git a/bookwyrm/migrations/0022_auto_20201212_1744.py b/bookwyrm/migrations/0022_auto_20201212_1744.py index 0a98597f8..2651578cc 100644 --- a/bookwyrm/migrations/0022_auto_20201212_1744.py +++ b/bookwyrm/migrations/0022_auto_20201212_1744.py @@ -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", ), ] diff --git a/bookwyrm/migrations/0023_auto_20201214_0511.py b/bookwyrm/migrations/0023_auto_20201214_0511.py index e811bded8..4b4a0c4a2 100644 --- a/bookwyrm/migrations/0023_auto_20201214_0511.py +++ b/bookwyrm/migrations/0023_auto_20201214_0511.py @@ -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, + ), ), ] diff --git a/bookwyrm/migrations/0023_merge_20201216_0112.py b/bookwyrm/migrations/0023_merge_20201216_0112.py index e3af48496..be88546e4 100644 --- a/bookwyrm/migrations/0023_merge_20201216_0112.py +++ b/bookwyrm/migrations/0023_merge_20201216_0112.py @@ -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 = [] diff --git a/bookwyrm/migrations/0024_merge_20201216_1721.py b/bookwyrm/migrations/0024_merge_20201216_1721.py index 41f81335e..bb944d4eb 100644 --- a/bookwyrm/migrations/0024_merge_20201216_1721.py +++ b/bookwyrm/migrations/0024_merge_20201216_1721.py @@ -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 = [] diff --git a/bookwyrm/migrations/0025_auto_20201217_0046.py b/bookwyrm/migrations/0025_auto_20201217_0046.py index a3ffe8c13..82e1f5037 100644 --- a/bookwyrm/migrations/0025_auto_20201217_0046.py +++ b/bookwyrm/migrations/0025_auto_20201217_0046.py @@ -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=""), ), ] diff --git a/bookwyrm/migrations/0026_status_content_warning.py b/bookwyrm/migrations/0026_status_content_warning.py index f4e494db9..5212e83a0 100644 --- a/bookwyrm/migrations/0026_status_content_warning.py +++ b/bookwyrm/migrations/0026_status_content_warning.py @@ -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 + ), ), ] diff --git a/bookwyrm/migrations/0027_auto_20201220_2007.py b/bookwyrm/migrations/0027_auto_20201220_2007.py index a3ad4dda3..5eec5139d 100644 --- a/bookwyrm/migrations/0027_auto_20201220_2007.py +++ b/bookwyrm/migrations/0027_auto_20201220_2007.py @@ -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), ), ] diff --git a/bookwyrm/migrations/0028_remove_book_author_text.py b/bookwyrm/migrations/0028_remove_book_author_text.py index 8743c910d..1f91d1c1e 100644 --- a/bookwyrm/migrations/0028_remove_book_author_text.py +++ b/bookwyrm/migrations/0028_remove_book_author_text.py @@ -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", ), ] diff --git a/bookwyrm/migrations/0029_auto_20201221_2014.py b/bookwyrm/migrations/0029_auto_20201221_2014.py index ebf27a742..7a6b71801 100644 --- a/bookwyrm/migrations/0029_auto_20201221_2014.py +++ b/bookwyrm/migrations/0029_auto_20201221_2014.py @@ -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), ), ] diff --git a/bookwyrm/migrations/0030_auto_20201224_1939.py b/bookwyrm/migrations/0030_auto_20201224_1939.py index 6de5d37fb..beee20c4b 100644 --- a/bookwyrm/migrations/0030_auto_20201224_1939.py +++ b/bookwyrm/migrations/0030_auto_20201224_1939.py @@ -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], + ), ), ] diff --git a/bookwyrm/migrations/0031_auto_20210104_2040.py b/bookwyrm/migrations/0031_auto_20210104_2040.py index 604392d41..c6418fc9d 100644 --- a/bookwyrm/migrations/0031_auto_20210104_2040.py +++ b/bookwyrm/migrations/0031_auto_20210104_2040.py @@ -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/"), ), ] diff --git a/bookwyrm/migrations/0032_auto_20210104_2055.py b/bookwyrm/migrations/0032_auto_20210104_2055.py index 692cd581f..8b8012dab 100644 --- a/bookwyrm/migrations/0032_auto_20210104_2055.py +++ b/bookwyrm/migrations/0032_auto_20210104_2055.py @@ -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"), ), ] diff --git a/bookwyrm/migrations/0033_siteinvite_created_date.py b/bookwyrm/migrations/0033_siteinvite_created_date.py index 9a3f98963..36d489ebb 100644 --- a/bookwyrm/migrations/0033_siteinvite_created_date.py +++ b/bookwyrm/migrations/0033_siteinvite_created_date.py @@ -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, ), ] diff --git a/bookwyrm/migrations/0034_importjob_complete.py b/bookwyrm/migrations/0034_importjob_complete.py index 141706070..6593df9fd 100644 --- a/bookwyrm/migrations/0034_importjob_complete.py +++ b/bookwyrm/migrations/0034_importjob_complete.py @@ -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), ), ] diff --git a/bookwyrm/migrations/0035_edition_edition_rank.py b/bookwyrm/migrations/0035_edition_edition_rank.py index 1a75a0974..7465c31b4 100644 --- a/bookwyrm/migrations/0035_edition_edition_rank.py +++ b/bookwyrm/migrations/0035_edition_edition_rank.py @@ -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), diff --git a/bookwyrm/migrations/0036_annualgoal.py b/bookwyrm/migrations/0036_annualgoal.py index fb12833ea..fd08fb247 100644 --- a/bookwyrm/migrations/0036_annualgoal.py +++ b/bookwyrm/migrations/0036_annualgoal.py @@ -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")}, }, ), ] diff --git a/bookwyrm/migrations/0037_auto_20210118_1954.py b/bookwyrm/migrations/0037_auto_20210118_1954.py index 97ba8808a..a0c27d457 100644 --- a/bookwyrm/migrations/0037_auto_20210118_1954.py +++ b/bookwyrm/migrations/0037_auto_20210118_1954.py @@ -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), ), ] diff --git a/bookwyrm/migrations/0038_auto_20210119_1534.py b/bookwyrm/migrations/0038_auto_20210119_1534.py index ac7a0d68f..14fd1ff29 100644 --- a/bookwyrm/migrations/0038_auto_20210119_1534.py +++ b/bookwyrm/migrations/0038_auto_20210119_1534.py @@ -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)] + ), ), ] diff --git a/bookwyrm/migrations/0039_merge_20210120_0753.py b/bookwyrm/migrations/0039_merge_20210120_0753.py index 1af40ee93..e698d8eaf 100644 --- a/bookwyrm/migrations/0039_merge_20210120_0753.py +++ b/bookwyrm/migrations/0039_merge_20210120_0753.py @@ -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 = [] diff --git a/bookwyrm/migrations/0040_auto_20210122_0057.py b/bookwyrm/migrations/0040_auto_20210122_0057.py index 8e528a899..0641f5273 100644 --- a/bookwyrm/migrations/0040_auto_20210122_0057.py +++ b/bookwyrm/migrations/0040_auto_20210122_0057.py @@ -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)], + ), ), ] diff --git a/bookwyrm/migrations/0041_auto_20210131_1614.py b/bookwyrm/migrations/0041_auto_20210131_1614.py index 6fcf406bd..01085dea3 100644 --- a/bookwyrm/migrations/0041_auto_20210131_1614.py +++ b/bookwyrm/migrations/0041_auto_20210131_1614.py @@ -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 + ), ), ] diff --git a/bookwyrm/migrations/0042_auto_20210201_2108.py b/bookwyrm/migrations/0042_auto_20210201_2108.py index 95a144de2..ee7201c10 100644 --- a/bookwyrm/migrations/0042_auto_20210201_2108.py +++ b/bookwyrm/migrations/0042_auto_20210201_2108.py @@ -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, + ), ), ] diff --git a/bookwyrm/migrations/0043_auto_20210204_2223.py b/bookwyrm/migrations/0043_auto_20210204_2223.py index b9c328eaf..2e8318c55 100644 --- a/bookwyrm/migrations/0043_auto_20210204_2223.py +++ b/bookwyrm/migrations/0043_auto_20210204_2223.py @@ -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", ), ] diff --git a/bookwyrm/migrations/0044_auto_20210207_1924.py b/bookwyrm/migrations/0044_auto_20210207_1924.py index 7289c73d8..897e8e025 100644 --- a/bookwyrm/migrations/0044_auto_20210207_1924.py +++ b/bookwyrm/migrations/0044_auto_20210207_1924.py @@ -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, ), ] diff --git a/bookwyrm/migrations/0045_auto_20210210_2114.py b/bookwyrm/migrations/0045_auto_20210210_2114.py index 87b9a3188..22f33cf47 100644 --- a/bookwyrm/migrations/0045_auto_20210210_2114.py +++ b/bookwyrm/migrations/0045_auto_20210210_2114.py @@ -8,51 +8,102 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0044_auto_20210207_1924'), + ("bookwyrm", "0044_auto_20210207_1924"), ] operations = [ migrations.RemoveConstraint( - model_name='notification', - name='notification_type_valid', + model_name="notification", + name="notification_type_valid", ), migrations.AddField( - model_name='notification', - name='related_list_item', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ListItem'), + model_name="notification", + name="related_list_item", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.ListItem", + ), ), migrations.AlterField( - model_name='notification', - name='notification_type', - field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import'), ('ADD', 'Add')], max_length=255), + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ("ADD", "Add"), + ], + max_length=255, + ), ), migrations.AlterField( - model_name='notification', - name='related_book', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.Edition'), + model_name="notification", + name="related_book", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.Edition", + ), ), migrations.AlterField( - model_name='notification', - name='related_import', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ImportJob'), + model_name="notification", + name="related_import", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.ImportJob", + ), ), migrations.AlterField( - model_name='notification', - name='related_status', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.Status'), + model_name="notification", + name="related_status", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.Status", + ), ), migrations.AlterField( - model_name='notification', - name='related_user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_user', to=settings.AUTH_USER_MODEL), + model_name="notification", + name="related_user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="related_user", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='notification', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="notification", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), migrations.AddConstraint( - model_name='notification', - constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT', 'ADD']), name='notification_type_valid'), + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "MENTION", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT", + "ADD", + ] + ), + name="notification_type_valid", + ), ), ] diff --git a/bookwyrm/migrations/0046_reviewrating.py b/bookwyrm/migrations/0046_reviewrating.py new file mode 100644 index 000000000..8d1490042 --- /dev/null +++ b/bookwyrm/migrations/0046_reviewrating.py @@ -0,0 +1,66 @@ +# Generated by Django 3.0.7 on 2021-02-25 18:36 + +from django.db import migrations, models +from django.db import connection +from django.db.models import Q +import django.db.models.deletion +from psycopg2.extras import execute_values + + +def convert_review_rating(app_registry, schema_editor): + """ take rating type Reviews and convert them to ReviewRatings """ + db_alias = schema_editor.connection.alias + + reviews = ( + app_registry.get_model("bookwyrm", "Review") + .objects.using(db_alias) + .filter(Q(content__isnull=True) | Q(content="")) + ) + + with connection.cursor() as cursor: + values = [(r.id,) for r in reviews] + execute_values( + cursor, + """ +INSERT INTO bookwyrm_reviewrating(review_ptr_id) +VALUES %s""", + values, + ) + + +def unconvert_review_rating(app_registry, schema_editor): + """ undo the conversion from ratings back to reviews""" + # All we need to do to revert this is drop the table, which Django will do + # on its own, as long as we have a valid reverse function. So, this is a + # no-op function so Django will do its thing + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0045_auto_20210210_2114"), + ] + + operations = [ + migrations.CreateModel( + name="ReviewRating", + fields=[ + ( + "review_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.Review", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("bookwyrm.review",), + ), + migrations.RunPython(convert_review_rating, unconvert_review_rating), + ] diff --git a/bookwyrm/migrations/0046_sitesettings_privacy_policy.py b/bookwyrm/migrations/0046_sitesettings_privacy_policy.py index 0c49d607c..f9193764c 100644 --- a/bookwyrm/migrations/0046_sitesettings_privacy_policy.py +++ b/bookwyrm/migrations/0046_sitesettings_privacy_policy.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0045_auto_20210210_2114'), + ("bookwyrm", "0045_auto_20210210_2114"), ] operations = [ migrations.AddField( - model_name='sitesettings', - name='privacy_policy', - field=models.TextField(default='Add a privacy policy here.'), + model_name="sitesettings", + name="privacy_policy", + field=models.TextField(default="Add a privacy policy here."), ), ] diff --git a/bookwyrm/migrations/0047_connector_isbn_search_url.py b/bookwyrm/migrations/0047_connector_isbn_search_url.py index 617a89d9d..2ca802c5a 100644 --- a/bookwyrm/migrations/0047_connector_isbn_search_url.py +++ b/bookwyrm/migrations/0047_connector_isbn_search_url.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0046_sitesettings_privacy_policy'), + ("bookwyrm", "0046_sitesettings_privacy_policy"), ] operations = [ migrations.AddField( - model_name='connector', - name='isbn_search_url', + model_name="connector", + name="isbn_search_url", field=models.CharField(blank=True, max_length=255, null=True), ), ] diff --git a/bookwyrm/migrations/0047_merge_20210228_1839.py b/bookwyrm/migrations/0047_merge_20210228_1839.py new file mode 100644 index 000000000..4be39e56f --- /dev/null +++ b/bookwyrm/migrations/0047_merge_20210228_1839.py @@ -0,0 +1,13 @@ +# Generated by Django 3.0.7 on 2021-02-28 18:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0046_reviewrating"), + ("bookwyrm", "0046_sitesettings_privacy_policy"), + ] + + operations = [] diff --git a/bookwyrm/migrations/0048_merge_20210308_1754.py b/bookwyrm/migrations/0048_merge_20210308_1754.py new file mode 100644 index 000000000..47fa9e771 --- /dev/null +++ b/bookwyrm/migrations/0048_merge_20210308_1754.py @@ -0,0 +1,13 @@ +# Generated by Django 3.0.7 on 2021-03-08 17:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0047_connector_isbn_search_url"), + ("bookwyrm", "0047_merge_20210228_1839"), + ] + + operations = [] diff --git a/bookwyrm/migrations/0049_auto_20210309_0156.py b/bookwyrm/migrations/0049_auto_20210309_0156.py new file mode 100644 index 000000000..ae9d77a89 --- /dev/null +++ b/bookwyrm/migrations/0049_auto_20210309_0156.py @@ -0,0 +1,113 @@ +# Generated by Django 3.0.7 on 2021-03-09 01:56 + +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.db.models.expressions + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0048_merge_20210308_1754"), + ] + + operations = [ + migrations.CreateModel( + name="Report", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "remote_id", + bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + ("note", models.TextField(blank=True, null=True)), + ("resolved", models.BooleanField(default=False)), + ( + "reporter", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reporter", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "statuses", + models.ManyToManyField(blank=True, null=True, to="bookwyrm.Status"), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="ReportComment", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "remote_id", + bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + ("note", models.TextField()), + ( + "report", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Report", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="report", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, reporter=django.db.models.expressions.F("user") + ), + name="self_report", + ), + ), + ] diff --git a/bookwyrm/migrations/0050_auto_20210313_0030.py b/bookwyrm/migrations/0050_auto_20210313_0030.py new file mode 100644 index 000000000..8c81c452b --- /dev/null +++ b/bookwyrm/migrations/0050_auto_20210313_0030.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.7 on 2021-03-13 00:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0049_auto_20210309_0156"), + ] + + operations = [ + migrations.AlterModelOptions( + name="report", + options={"ordering": ("-created_date",)}, + ), + migrations.AlterModelOptions( + name="reportcomment", + options={"ordering": ("-created_date",)}, + ), + migrations.AlterField( + model_name="report", + name="statuses", + field=models.ManyToManyField(blank=True, to="bookwyrm.Status"), + ), + ] diff --git a/bookwyrm/migrations/0051_auto_20210316_1950.py b/bookwyrm/migrations/0051_auto_20210316_1950.py new file mode 100644 index 000000000..3caecbbe1 --- /dev/null +++ b/bookwyrm/migrations/0051_auto_20210316_1950.py @@ -0,0 +1,66 @@ +# Generated by Django 3.0.7 on 2021-03-16 19:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0050_auto_20210313_0030"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="notification", + name="notification_type_valid", + ), + migrations.AddField( + model_name="notification", + name="related_report", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="bookwyrm.Report", + ), + ), + migrations.AlterField( + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ("ADD", "Add"), + ("REPORT", "Report"), + ], + max_length=255, + ), + ), + migrations.AddConstraint( + model_name="notification", + constraint=models.CheckConstraint( + check=models.Q( + notification_type__in=[ + "FAVORITE", + "REPLY", + "MENTION", + "TAG", + "FOLLOW", + "FOLLOW_REQUEST", + "BOOST", + "IMPORT", + "ADD", + "REPORT", + ] + ), + name="notification_type_valid", + ), + ), + ] diff --git a/bookwyrm/migrations/0052_user_show_goal.py b/bookwyrm/migrations/0052_user_show_goal.py new file mode 100644 index 000000000..3b72ee7ac --- /dev/null +++ b/bookwyrm/migrations/0052_user_show_goal.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2021-03-18 15:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0051_auto_20210316_1950"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="show_goal", + field=models.BooleanField(default=True), + ), + ] diff --git a/bookwyrm/migrations/0053_auto_20210319_1913.py b/bookwyrm/migrations/0053_auto_20210319_1913.py new file mode 100644 index 000000000..023319b39 --- /dev/null +++ b/bookwyrm/migrations/0053_auto_20210319_1913.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.6 on 2021-03-19 19:13 + +import bookwyrm.models.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0052_user_show_goal"), + ] + + operations = [ + migrations.AlterField( + model_name="review", + name="rating", + field=bookwyrm.models.fields.DecimalField( + blank=True, + decimal_places=2, + default=None, + max_digits=3, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ] diff --git a/bookwyrm/migrations/0054_auto_20210319_1942.py b/bookwyrm/migrations/0054_auto_20210319_1942.py new file mode 100644 index 000000000..5d5865b67 --- /dev/null +++ b/bookwyrm/migrations/0054_auto_20210319_1942.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.6 on 2021-03-19 19:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0053_auto_20210319_1913"), + ] + + operations = [ + migrations.AlterField( + model_name="importitem", + name="data", + field=models.JSONField(), + ), + migrations.AlterField( + model_name="user", + name="first_name", + field=models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 0aef63850..326a673e1 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -1,4 +1,4 @@ -''' bring all the models into the app namespace ''' +""" bring all the models into the app namespace """ import inspect import sys @@ -9,7 +9,8 @@ from .connector import Connector from .shelf import Shelf, ShelfBook from .list import List, ListItem -from .status import Status, GeneratedNote, Review, Comment, Quotation +from .status import Status, GeneratedNote, Comment, Quotation +from .status import Review, ReviewRating from .status import Boost from .attachment import Image from .favorite import Favorite @@ -20,6 +21,7 @@ from .tag import Tag, UserTag from .user import User, KeyPair, AnnualGoal from .relationship import UserFollows, UserFollowRequest, UserBlocks +from .report import Report, ReportComment from .federated_server import FederatedServer from .import_job import ImportJob, ImportItem @@ -27,8 +29,12 @@ from .import_job import ImportJob, ImportItem from .site import SiteSettings, SiteInvite, PasswordReset cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) -activity_models = {c[1].activity_serializer.__name__: c[1] \ - for c in cls_members if hasattr(c[1], 'activity_serializer')} +activity_models = { + c[1].activity_serializer.__name__: c[1] + for c in cls_members + if hasattr(c[1], "activity_serializer") +} status_models = [ - c.__name__ for (_, c) in activity_models.items() if issubclass(c, Status)] + c.__name__ for (_, c) in activity_models.items() if issubclass(c, Status) +] diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 10015bf14..a253207ac 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -1,4 +1,4 @@ -''' activitypub model functionality ''' +""" activitypub model functionality """ from base64 import b64encode from functools import reduce import json @@ -26,18 +26,19 @@ logger = logging.getLogger(__name__) # I tried to separate these classes into mutliple files but I kept getting # circular import errors so I gave up. I'm sure it could be done though! class ActivitypubMixin: - ''' add this mixin for models that are AP serializable ''' + """ add this mixin for models that are AP serializable """ + activity_serializer = lambda: {} reverse_unfurl = False def __init__(self, *args, **kwargs): - ''' collect some info on model fields ''' + """ collect some info on model fields """ self.image_fields = [] self.many_to_many_fields = [] - self.simple_fields = [] # "simple" + self.simple_fields = [] # "simple" # sort model fields by type for field in self._meta.get_fields(): - if not hasattr(field, 'field_to_activity'): + if not hasattr(field, "field_to_activity"): continue if isinstance(field, ImageField): @@ -48,33 +49,41 @@ class ActivitypubMixin: self.simple_fields.append(field) # a list of allll the serializable fields - self.activity_fields = self.image_fields + \ - self.many_to_many_fields + self.simple_fields + self.activity_fields = ( + self.image_fields + self.many_to_many_fields + self.simple_fields + ) # these are separate to avoid infinite recursion issues - self.deserialize_reverse_fields = self.deserialize_reverse_fields \ - if hasattr(self, 'deserialize_reverse_fields') else [] - self.serialize_reverse_fields = self.serialize_reverse_fields \ - if hasattr(self, 'serialize_reverse_fields') else [] + self.deserialize_reverse_fields = ( + self.deserialize_reverse_fields + if hasattr(self, "deserialize_reverse_fields") + else [] + ) + self.serialize_reverse_fields = ( + self.serialize_reverse_fields + if hasattr(self, "serialize_reverse_fields") + else [] + ) super().__init__(*args, **kwargs) - @classmethod def find_existing_by_remote_id(cls, remote_id): - ''' look up a remote id in the db ''' - return cls.find_existing({'id': remote_id}) + """ look up a remote id in the db """ + return cls.find_existing({"id": remote_id}) @classmethod def find_existing(cls, data): - ''' compare data to fields that can be used for deduplation. + """compare data to fields that can be used for deduplation. This always includes remote_id, but can also be unique identifiers - like an isbn for an edition ''' + like an isbn for an edition""" filters = [] # grabs all the data from the model to create django queryset filters for field in cls._meta.get_fields(): - if not hasattr(field, 'deduplication_field') or \ - not field.deduplication_field: + if ( + not hasattr(field, "deduplication_field") + or not field.deduplication_field + ): continue value = data.get(field.get_activitypub_field()) @@ -82,9 +91,9 @@ class ActivitypubMixin: continue filters.append({field.name: value}) - if hasattr(cls, 'origin_id') and 'id' in data: + if hasattr(cls, "origin_id") and "id" in data: # kinda janky, but this handles special case for books - filters.append({'origin_id': data['id']}) + filters.append({"origin_id": data["id"]}) if not filters: # if there are no deduplication fields, it will match the first @@ -92,45 +101,41 @@ class ActivitypubMixin: return None objects = cls.objects - if hasattr(objects, 'select_subclasses'): + if hasattr(objects, "select_subclasses"): objects = objects.select_subclasses() # an OR operation on all the match fields, sorry for the dense syntax - match = objects.filter( - reduce(operator.or_, (Q(**f) for f in filters)) - ) + match = objects.filter(reduce(operator.or_, (Q(**f) for f in filters))) # there OUGHT to be only one match return match.first() - def broadcast(self, activity, sender, software=None): - ''' send out an activity ''' + """ send out an activity """ broadcast_task.delay( sender.id, json.dumps(activity, cls=activitypub.ActivityEncoder), - self.get_recipients(software=software) + self.get_recipients(software=software), ) - def get_recipients(self, software=None): - ''' figure out which inbox urls to post to ''' + """ figure out which inbox urls to post to """ # first we have to figure out who should receive this activity - privacy = self.privacy if hasattr(self, 'privacy') else 'public' + privacy = self.privacy if hasattr(self, "privacy") else "public" # is this activity owned by a user (statuses, lists, shelves), or is it # general to the instance (like books) - user = self.user if hasattr(self, 'user') else None - user_model = apps.get_model('bookwyrm.User', require_ready=True) + user = self.user if hasattr(self, "user") else None + user_model = apps.get_model("bookwyrm.User", require_ready=True) if not user and isinstance(self, user_model): # or maybe the thing itself is a user user = self # find anyone who's tagged in a status, for example - mentions = self.recipients if hasattr(self, 'recipients') else [] + mentions = self.recipients if hasattr(self, "recipients") else [] # we always send activities to explicitly mentioned users' inboxes recipients = [u.inbox for u in mentions or []] # unless it's a dm, all the followers should receive the activity - if privacy != 'direct': + if privacy != "direct": # we will send this out to a subset of all remote users queryset = user_model.objects.filter( local=False, @@ -138,43 +143,43 @@ class ActivitypubMixin: # filter users first by whether they're using the desired software # this lets us send book updates only to other bw servers if software: - queryset = queryset.filter( - bookwyrm_user=(software == 'bookwyrm') - ) + queryset = queryset.filter(bookwyrm_user=(software == "bookwyrm")) # if there's a user, we only want to send to the user's followers if user: queryset = queryset.filter(following=user) # ideally, we will send to shared inboxes for efficiency - shared_inboxes = queryset.filter( - shared_inbox__isnull=False - ).values_list('shared_inbox', flat=True).distinct() + shared_inboxes = ( + queryset.filter(shared_inbox__isnull=False) + .values_list("shared_inbox", flat=True) + .distinct() + ) # but not everyone has a shared inbox - inboxes = queryset.filter( - shared_inbox__isnull=True - ).values_list('inbox', flat=True) + inboxes = queryset.filter(shared_inbox__isnull=True).values_list( + "inbox", flat=True + ) recipients += list(shared_inboxes) + list(inboxes) return recipients - def to_activity_dataclass(self): - ''' convert from a model to an activity ''' + """ convert from a model to an activity """ activity = generate_activity(self) return self.activity_serializer(**activity) - def to_activity(self, **kwargs): # pylint: disable=unused-argument - ''' convert from a model to a json activity ''' + def to_activity(self, **kwargs): # pylint: disable=unused-argument + """ convert from a model to a json activity """ return self.to_activity_dataclass().serialize() class ObjectMixin(ActivitypubMixin): - ''' add this mixin for object models that are AP serializable ''' + """ add this mixin for object models that are AP serializable """ + def save(self, *args, created=None, **kwargs): - ''' broadcast created/updated/deleted objects as appropriate ''' - broadcast = kwargs.get('broadcast', True) + """ broadcast created/updated/deleted objects as appropriate """ + broadcast = kwargs.get("broadcast", True) # this bonus kwarg woul cause an error in the base save method - if 'broadcast' in kwargs: - del kwargs['broadcast'] + if "broadcast" in kwargs: + del kwargs["broadcast"] created = created or not bool(self.id) # first off, we want to save normally no matter what @@ -183,7 +188,7 @@ class ObjectMixin(ActivitypubMixin): return # this will work for objects owned by a user (lists, shelves) - user = self.user if hasattr(self, 'user') else None + user = self.user if hasattr(self, "user") else None if created: # broadcast Create activities for objects owned by a local user @@ -193,10 +198,10 @@ class ObjectMixin(ActivitypubMixin): try: software = None # do we have a "pure" activitypub version of this for mastodon? - if hasattr(self, 'pure_content'): + if hasattr(self, "pure_content"): pure_activity = self.to_create_activity(user, pure=True) - self.broadcast(pure_activity, user, software='other') - software = 'bookwyrm' + self.broadcast(pure_activity, user, software="other") + software = "bookwyrm" # sends to BW only if we just did a pure version for masto activity = self.to_create_activity(user) self.broadcast(activity, user, software=software) @@ -209,39 +214,38 @@ class ObjectMixin(ActivitypubMixin): # --- updating an existing object if not user: # users don't have associated users, they ARE users - user_model = apps.get_model('bookwyrm.User', require_ready=True) + user_model = apps.get_model("bookwyrm.User", require_ready=True) if isinstance(self, user_model): user = self # book data tracks last editor - elif hasattr(self, 'last_edited_by'): + elif hasattr(self, "last_edited_by"): user = self.last_edited_by # again, if we don't know the user or they're remote, don't bother if not user or not user.local: return # is this a deletion? - if hasattr(self, 'deleted') and self.deleted: + if hasattr(self, "deleted") and self.deleted: activity = self.to_delete_activity(user) else: activity = self.to_update_activity(user) self.broadcast(activity, user) - def to_create_activity(self, user, **kwargs): - ''' returns the object wrapped in a Create activity ''' + """ returns the object wrapped in a Create activity """ activity_object = self.to_activity_dataclass(**kwargs) signature = None - create_id = self.remote_id + '/activity' - if hasattr(activity_object, 'content') and activity_object.content: + create_id = self.remote_id + "/activity" + if hasattr(activity_object, "content") and activity_object.content: signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key)) content = activity_object.content - signed_message = signer.sign(SHA256.new(content.encode('utf8'))) + signed_message = signer.sign(SHA256.new(content.encode("utf8"))) signature = activitypub.Signature( - creator='%s#main-key' % user.remote_id, + creator="%s#main-key" % user.remote_id, created=activity_object.published, - signatureValue=b64encode(signed_message).decode('utf8') + signatureValue=b64encode(signed_message).decode("utf8"), ) return activitypub.Create( @@ -253,50 +257,48 @@ class ObjectMixin(ActivitypubMixin): signature=signature, ).serialize() - def to_delete_activity(self, user): - ''' notice of deletion ''' + """ notice of deletion """ return activitypub.Delete( - id=self.remote_id + '/activity', + id=self.remote_id + "/activity", actor=user.remote_id, - to=['%s/followers' % user.remote_id], - cc=['https://www.w3.org/ns/activitystreams#Public'], + to=["%s/followers" % user.remote_id], + cc=["https://www.w3.org/ns/activitystreams#Public"], + object=self, + ).serialize() + + def to_update_activity(self, user): + """ wrapper for Updates to an activity """ + activity_id = "%s#update/%s" % (self.remote_id, uuid4()) + return activitypub.Update( + id=activity_id, + actor=user.remote_id, + to=["https://www.w3.org/ns/activitystreams#Public"], object=self, ).serialize() - def to_update_activity(self, user): - ''' wrapper for Updates to an activity ''' - activity_id = '%s#update/%s' % (self.remote_id, uuid4()) - return activitypub.Update( - id=activity_id, - actor=user.remote_id, - to=['https://www.w3.org/ns/activitystreams#Public'], - object=self - ).serialize() - - class OrderedCollectionPageMixin(ObjectMixin): - ''' just the paginator utilities, so you don't HAVE to - override ActivitypubMixin's to_activity (ie, for outbox) ''' + """just the paginator utilities, so you don't HAVE to + override ActivitypubMixin's to_activity (ie, for outbox)""" + @property def collection_remote_id(self): - ''' this can be overriden if there's a special remote id, ie outbox ''' + """ this can be overriden if there's a special remote id, ie outbox """ return self.remote_id - - def to_ordered_collection(self, queryset, \ - remote_id=None, page=False, collection_only=False, **kwargs): - ''' an ordered collection of whatevers ''' + def to_ordered_collection( + self, queryset, remote_id=None, page=False, collection_only=False, **kwargs + ): + """ an ordered collection of whatevers """ if not queryset.ordered: - raise RuntimeError('queryset must be ordered') + raise RuntimeError("queryset must be ordered") remote_id = remote_id or self.remote_id if page: - return to_ordered_collection_page( - queryset, remote_id, **kwargs) + return to_ordered_collection_page(queryset, remote_id, **kwargs) - if collection_only or not hasattr(self, 'activity_serializer'): + if collection_only or not hasattr(self, "activity_serializer"): serializer = activitypub.OrderedCollection activity = {} else: @@ -305,23 +307,24 @@ class OrderedCollectionPageMixin(ObjectMixin): activity = generate_activity(self) if remote_id: - activity['id'] = remote_id + activity["id"] = remote_id paginated = Paginator(queryset, PAGE_LENGTH) # add computed fields specific to orderd collections - activity['totalItems'] = paginated.count - activity['first'] = '%s?page=1' % remote_id - activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages) + activity["totalItems"] = paginated.count + activity["first"] = "%s?page=1" % remote_id + activity["last"] = "%s?page=%d" % (remote_id, paginated.num_pages) return serializer(**activity) class OrderedCollectionMixin(OrderedCollectionPageMixin): - ''' extends activitypub models to work as ordered collections ''' + """ extends activitypub models to work as ordered collections """ + @property def collection_queryset(self): - ''' usually an ordered collection model aggregates a different model ''' - raise NotImplementedError('Model must define collection_queryset') + """ usually an ordered collection model aggregates a different model """ + raise NotImplementedError("Model must define collection_queryset") activity_serializer = activitypub.OrderedCollection @@ -329,18 +332,20 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin): return self.to_ordered_collection(self.collection_queryset, **kwargs) def to_activity(self, **kwargs): - ''' an ordered collection of the specified model queryset ''' + """ an ordered collection of the specified model queryset """ return self.to_ordered_collection( - self.collection_queryset, **kwargs).serialize() + self.collection_queryset, **kwargs + ).serialize() class CollectionItemMixin(ActivitypubMixin): - ''' for items that are part of an (Ordered)Collection ''' + """ for items that are part of an (Ordered)Collection """ + activity_serializer = activitypub.Add object_field = collection_field = None def save(self, *args, broadcast=True, **kwargs): - ''' broadcast updated ''' + """ broadcast updated """ created = not bool(self.id) # first off, we want to save normally no matter what super().save(*args, **kwargs) @@ -353,113 +358,120 @@ class CollectionItemMixin(ActivitypubMixin): activity = self.to_add_activity() self.broadcast(activity, self.user) - def delete(self, *args, **kwargs): - ''' broadcast a remove activity ''' + """ broadcast a remove activity """ activity = self.to_remove_activity() super().delete(*args, **kwargs) - self.broadcast(activity, self.user) - + if self.user.local: + self.broadcast(activity, self.user) def to_add_activity(self): - ''' AP for shelving a book''' + """ AP for shelving a book""" object_field = getattr(self, self.object_field) collection_field = getattr(self, self.collection_field) return activitypub.Add( - id='%s#add' % self.remote_id, + id=self.remote_id, actor=self.user.remote_id, object=object_field, - target=collection_field.remote_id + target=collection_field.remote_id, ).serialize() def to_remove_activity(self): - ''' AP for un-shelving a book''' + """ AP for un-shelving a book""" object_field = getattr(self, self.object_field) collection_field = getattr(self, self.collection_field) return activitypub.Remove( - id='%s#remove' % self.remote_id, + id=self.remote_id, actor=self.user.remote_id, object=object_field, - target=collection_field.remote_id + target=collection_field.remote_id, ).serialize() class ActivityMixin(ActivitypubMixin): - ''' add this mixin for models that are AP serializable ''' + """ add this mixin for models that are AP serializable """ + def save(self, *args, broadcast=True, **kwargs): - ''' broadcast activity ''' + """ broadcast activity """ super().save(*args, **kwargs) - user = self.user if hasattr(self, 'user') else self.user_subject + user = self.user if hasattr(self, "user") else self.user_subject if broadcast and user.local: self.broadcast(self.to_activity(), user) - def delete(self, *args, broadcast=True, **kwargs): - ''' nevermind, undo that activity ''' - user = self.user if hasattr(self, 'user') else self.user_subject + """ nevermind, undo that activity """ + user = self.user if hasattr(self, "user") else self.user_subject if broadcast and user.local: self.broadcast(self.to_undo_activity(), user) super().delete(*args, **kwargs) - def to_undo_activity(self): - ''' undo an action ''' - user = self.user if hasattr(self, 'user') else self.user_subject + """ undo an action """ + user = self.user if hasattr(self, "user") else self.user_subject return activitypub.Undo( - id='%s#undo' % self.remote_id, + id="%s#undo" % self.remote_id, actor=user.remote_id, object=self, ).serialize() def generate_activity(obj): - ''' go through the fields on an object ''' + """ go through the fields on an object """ activity = {} for field in obj.activity_fields: field.set_activity_from_field(activity, obj) - if hasattr(obj, 'serialize_reverse_fields'): + if hasattr(obj, "serialize_reverse_fields"): # for example, editions of a work - for model_field_name, activity_field_name, sort_field in \ - obj.serialize_reverse_fields: + for ( + model_field_name, + activity_field_name, + sort_field, + ) in obj.serialize_reverse_fields: related_field = getattr(obj, model_field_name) - activity[activity_field_name] = \ - unfurl_related_field(related_field, sort_field) + activity[activity_field_name] = unfurl_related_field( + related_field, sort_field + ) - if not activity.get('id'): - activity['id'] = obj.get_remote_id() + if not activity.get("id"): + activity["id"] = obj.get_remote_id() return activity def unfurl_related_field(related_field, sort_field=None): - ''' load reverse lookups (like public key owner or Status attachment ''' - if hasattr(related_field, 'all'): - return [unfurl_related_field(i) for i in related_field.order_by( - sort_field).all()] + """ load reverse lookups (like public key owner or Status attachment """ + if hasattr(related_field, "all"): + return [ + unfurl_related_field(i) for i in related_field.order_by(sort_field).all() + ] if related_field.reverse_unfurl: - return related_field.field_to_activity() + # if it's a one-to-one (key pair) + if hasattr(related_field, "field_to_activity"): + return related_field.field_to_activity() + # if it's one-to-many (attachments) + return related_field.to_activity() return related_field.remote_id @app.task def broadcast_task(sender_id, activity, recipients): - ''' the celery task for broadcast ''' - user_model = apps.get_model('bookwyrm.User', require_ready=True) + """ the celery task for broadcast """ + user_model = apps.get_model("bookwyrm.User", require_ready=True) sender = user_model.objects.get(id=sender_id) for recipient in recipients: try: sign_and_send(sender, activity, recipient) - except (HTTPError, SSLError, ConnectionError) as e: - logger.exception(e) + except (HTTPError, SSLError, requests.exceptions.ConnectionError): + pass def sign_and_send(sender, data, destination): - ''' crpyto whatever and http junk ''' + """ crpyto whatever and http junk """ now = http_date() if not sender.key_pair.private_key: # this shouldn't happen. it would be bad if it happened. - raise ValueError('No private key found for sender') + raise ValueError("No private key found for sender") digest = make_digest(data) @@ -467,11 +479,11 @@ def sign_and_send(sender, data, destination): destination, data=data, headers={ - 'Date': now, - 'Digest': digest, - 'Signature': make_signature(sender, destination, now, digest), - 'Content-Type': 'application/activity+json; charset=utf-8', - 'User-Agent': USER_AGENT, + "Date": now, + "Digest": digest, + "Signature": make_signature(sender, destination, now, digest), + "Content-Type": "application/activity+json; charset=utf-8", + "User-Agent": USER_AGENT, }, ) if not response.ok: @@ -481,8 +493,9 @@ def sign_and_send(sender, data, destination): # pylint: disable=unused-argument def to_ordered_collection_page( - queryset, remote_id, id_only=False, page=1, pure=False, **kwargs): - ''' serialize and pagiante a queryset ''' + queryset, remote_id, id_only=False, page=1, pure=False, **kwargs +): + """ serialize and pagiante a queryset """ paginated = Paginator(queryset, PAGE_LENGTH) activity_page = paginated.page(page) @@ -493,14 +506,13 @@ def to_ordered_collection_page( prev_page = next_page = None if activity_page.has_next(): - next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number()) + next_page = "%s?page=%d" % (remote_id, activity_page.next_page_number()) if activity_page.has_previous(): - prev_page = '%s?page=%d' % \ - (remote_id, activity_page.previous_page_number()) + prev_page = "%s?page=%d" % (remote_id, activity_page.previous_page_number()) return activitypub.OrderedCollectionPage( - id='%s?page=%s' % (remote_id, page), + id="%s?page=%s" % (remote_id, page), partOf=remote_id, orderedItems=items, next=next_page, - prev=prev_page + prev=prev_page, ) diff --git a/bookwyrm/models/attachment.py b/bookwyrm/models/attachment.py index e3450a5ad..8d2238a14 100644 --- a/bookwyrm/models/attachment.py +++ b/bookwyrm/models/attachment.py @@ -1,4 +1,4 @@ -''' media that is posted in the app ''' +""" media that is posted in the app """ from django.db import models from bookwyrm import activitypub @@ -8,23 +8,29 @@ from . import fields class Attachment(ActivitypubMixin, BookWyrmModel): - ''' an image (or, in the future, video etc) associated with a status ''' + """ an image (or, in the future, video etc) associated with a status """ + status = models.ForeignKey( - 'Status', - on_delete=models.CASCADE, - related_name='attachments', - null=True + "Status", on_delete=models.CASCADE, related_name="attachments", null=True ) reverse_unfurl = True + class Meta: - ''' one day we'll have other types of attachments besides images ''' + """ one day we'll have other types of attachments besides images """ + abstract = True class Image(Attachment): - ''' an image attachment ''' + """ an image attachment """ + image = fields.ImageField( - upload_to='status/', null=True, blank=True, activitypub_field='url') - caption = fields.TextField(null=True, blank=True, activitypub_field='name') + upload_to="status/", + null=True, + blank=True, + activitypub_field="url", + alt_field="caption", + ) + caption = fields.TextField(null=True, blank=True, activitypub_field="name") activity_serializer = activitypub.Image diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index d0cb8d19b..4c5fe6c8f 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -1,4 +1,4 @@ -''' database schema for info about authors ''' +""" database schema for info about authors """ from django.db import models from bookwyrm import activitypub @@ -9,9 +9,11 @@ from . import fields class Author(BookDataModel): - ''' basic biographic info ''' + """ basic biographic info """ + wikipedia_link = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) # idk probably other keys would be useful here? born = fields.DateTimeField(blank=True, null=True) died = fields.DateTimeField(blank=True, null=True) @@ -22,7 +24,7 @@ class Author(BookDataModel): bio = fields.HtmlField(null=True, blank=True) def get_remote_id(self): - ''' editions and works both use "book" instead of model_name ''' - return 'https://%s/author/%s' % (DOMAIN, self.id) + """ editions and works both use "book" instead of model_name """ + return "https://%s/author/%s" % (DOMAIN, self.id) activity_serializer = activitypub.Author diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 7af487492..60e5da0ad 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -1,4 +1,4 @@ -''' base model with default fields ''' +""" base model with default fields """ from django.db import models from django.dispatch import receiver @@ -7,34 +7,36 @@ from .fields import RemoteIdField class BookWyrmModel(models.Model): - ''' shared fields ''' + """ shared fields """ + created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) - remote_id = RemoteIdField(null=True, activitypub_field='id') + remote_id = RemoteIdField(null=True, activitypub_field="id") def get_remote_id(self): - ''' generate a url that resolves to the local object ''' - base_path = 'https://%s' % DOMAIN - if hasattr(self, 'user'): - base_path = '%s%s' % (base_path, self.user.local_path) + """ generate a url that resolves to the local object """ + base_path = "https://%s" % DOMAIN + if hasattr(self, "user"): + base_path = "%s%s" % (base_path, self.user.local_path) model_name = type(self).__name__.lower() - return '%s/%s/%d' % (base_path, model_name, self.id) + return "%s/%s/%d" % (base_path, model_name, self.id) class Meta: - ''' this is just here to provide default fields for other models ''' + """ this is just here to provide default fields for other models """ + abstract = True @property def local_path(self): - ''' how to link to this object in the local app ''' - return self.get_remote_id().replace('https://%s' % DOMAIN, '') + """ how to link to this object in the local app """ + return self.get_remote_id().replace("https://%s" % DOMAIN, "") @receiver(models.signals.post_save) -#pylint: disable=unused-argument +# pylint: disable=unused-argument def execute_after_save(sender, instance, created, *args, **kwargs): - ''' set the remote_id after save (when the id is available) ''' - if not created or not hasattr(instance, 'get_remote_id'): + """ set the remote_id after save (when the id is available) """ + if not created or not hasattr(instance, "get_remote_id"): return if not instance.remote_id: instance.remote_id = instance.get_remote_id() diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 84bfbc6bd..3204c6035 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -1,7 +1,7 @@ -''' database schema for books and shelves ''' +""" database schema for books and shelves """ import re -from django.db import models +from django.db import models, transaction from model_utils.managers import InheritanceManager from bookwyrm import activitypub @@ -11,25 +11,30 @@ from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .base_model import BookWyrmModel from . import fields + class BookDataModel(ObjectMixin, BookWyrmModel): - ''' fields shared between editable book data (books, works, authors) ''' + """ fields shared between editable book data (books, works, authors) """ + origin_id = models.CharField(max_length=255, null=True, blank=True) openlibrary_key = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) librarything_key = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) goodreads_key = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) - last_edited_by = models.ForeignKey( - 'User', on_delete=models.PROTECT, null=True) + last_edited_by = models.ForeignKey("User", on_delete=models.PROTECT, null=True) class Meta: - ''' can't initialize this model, that wouldn't make sense ''' + """ can't initialize this model, that wouldn't make sense """ + abstract = True def save(self, *args, **kwargs): - ''' ensure that the remote_id is within this instance ''' + """ ensure that the remote_id is within this instance """ if self.id: self.remote_id = self.get_remote_id() else: @@ -37,15 +42,15 @@ class BookDataModel(ObjectMixin, BookWyrmModel): self.remote_id = None return super().save(*args, **kwargs) - def broadcast(self, activity, sender, software='bookwyrm'): - ''' only send book data updates to other bookwyrm instances ''' + def broadcast(self, activity, sender, software="bookwyrm"): + """ only send book data updates to other bookwyrm instances """ super().broadcast(activity, sender, software=software) class Book(BookDataModel): - ''' a generic book, which can mean either an edition or a work ''' - connector = models.ForeignKey( - 'Connector', on_delete=models.PROTECT, null=True) + """ a generic book, which can mean either an edition or a work """ + + connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True) # book/work metadata title = fields.CharField(max_length=255) @@ -63,9 +68,10 @@ class Book(BookDataModel): subject_places = fields.ArrayField( models.CharField(max_length=255), blank=True, null=True, default=list ) - authors = fields.ManyToManyField('Author') + authors = fields.ManyToManyField("Author") cover = fields.ImageField( - upload_to='covers/', blank=True, null=True, alt_field='alt_text') + upload_to="covers/", blank=True, null=True, alt_field="alt_text" + ) first_published_date = fields.DateTimeField(blank=True, null=True) published_date = fields.DateTimeField(blank=True, null=True) @@ -73,42 +79,44 @@ class Book(BookDataModel): @property def author_text(self): - ''' format a list of authors ''' - return ', '.join(a.name for a in self.authors.all()) + """ format a list of authors """ + return ", ".join(a.name for a in self.authors.all()) @property def latest_readthrough(self): - ''' most recent readthrough activity ''' - return self.readthrough_set.order_by('-updated_date').first() + """ most recent readthrough activity """ + return self.readthrough_set.order_by("-updated_date").first() @property def edition_info(self): - ''' properties of this edition, as a string ''' + """ properties of this edition, as a string """ items = [ - self.physical_format if hasattr(self, 'physical_format') else None, - self.languages[0] + ' language' if self.languages and \ - self.languages[0] != 'English' else None, + self.physical_format if hasattr(self, "physical_format") else None, + self.languages[0] + " language" + if self.languages and self.languages[0] != "English" + else None, str(self.published_date.year) if self.published_date else None, + ", ".join(self.publishers) if hasattr(self, "publishers") else None, ] - return ', '.join(i for i in items if i) + return ", ".join(i for i in items if i) @property def alt_text(self): - ''' image alt test ''' - text = '%s' % self.title + """ image alt test """ + text = "%s" % self.title if self.edition_info: - text += ' (%s)' % self.edition_info + text += " (%s)" % self.edition_info return text def save(self, *args, **kwargs): - ''' can't be abstract for query reasons, but you shouldn't USE it ''' + """ can't be abstract for query reasons, but you shouldn't USE it """ if not isinstance(self, Edition) and not isinstance(self, Work): - raise ValueError('Books should be added as Editions or Works') + raise ValueError("Books should be added as Editions or Works") return super().save(*args, **kwargs) def get_remote_id(self): - ''' editions and works both use "book" instead of model_name ''' - return 'https://%s/book/%d' % (DOMAIN, self.id) + """ editions and works both use "book" instead of model_name """ + return "https://%s/book/%d" % (DOMAIN, self.id) def __repr__(self): return "<{} key={!r} title={!r}>".format( @@ -119,77 +127,96 @@ class Book(BookDataModel): class Work(OrderedCollectionPageMixin, Book): - ''' a work (an abstract concept of a book that manifests in an edition) ''' + """ a work (an abstract concept of a book that manifests in an edition) """ + # library of congress catalog control number lccn = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) # this has to be nullable but should never be null default_edition = fields.ForeignKey( - 'Edition', - on_delete=models.PROTECT, - null=True, - load_remote=False + "Edition", on_delete=models.PROTECT, null=True, load_remote=False ) def save(self, *args, **kwargs): - ''' set some fields on the edition object ''' + """ set some fields on the edition object """ # set rank for edition in self.editions.all(): edition.save() return super().save(*args, **kwargs) def get_default_edition(self): - ''' in case the default edition is not set ''' - return self.default_edition or self.editions.order_by( - '-edition_rank' - ).first() + """ in case the default edition is not set """ + return self.default_edition or self.editions.order_by("-edition_rank").first() + + @transaction.atomic() + def reset_default_edition(self): + """ sets a new default edition based on computed rank """ + self.default_edition = None + # editions are re-ranked implicitly + self.save() + self.default_edition = self.get_default_edition() + self.save() def to_edition_list(self, **kwargs): - ''' an ordered collection of editions ''' + """ an ordered collection of editions """ return self.to_ordered_collection( - self.editions.order_by('-edition_rank').all(), - remote_id='%s/editions' % self.remote_id, + self.editions.order_by("-edition_rank").all(), + remote_id="%s/editions" % self.remote_id, **kwargs ) activity_serializer = activitypub.Work - serialize_reverse_fields = [('editions', 'editions', '-edition_rank')] - deserialize_reverse_fields = [('editions', 'editions')] + serialize_reverse_fields = [("editions", "editions", "-edition_rank")] + deserialize_reverse_fields = [("editions", "editions")] class Edition(Book): - ''' an edition of a book ''' + """ an edition of a book """ + # these identifiers only apply to editions, not works isbn_10 = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) isbn_13 = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) oclc_number = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) asin = fields.CharField( - max_length=255, blank=True, null=True, deduplication_field=True) + max_length=255, blank=True, null=True, deduplication_field=True + ) pages = fields.IntegerField(blank=True, null=True) physical_format = fields.CharField(max_length=255, blank=True, null=True) publishers = fields.ArrayField( models.CharField(max_length=255), blank=True, default=list ) shelves = models.ManyToManyField( - 'Shelf', + "Shelf", symmetrical=False, - through='ShelfBook', - through_fields=('book', 'shelf') + through="ShelfBook", + through_fields=("book", "shelf"), ) parent_work = fields.ForeignKey( - 'Work', on_delete=models.PROTECT, null=True, - related_name='editions', activitypub_field='work') + "Work", + on_delete=models.PROTECT, + null=True, + related_name="editions", + activitypub_field="work", + ) edition_rank = fields.IntegerField(default=0) activity_serializer = activitypub.Edition - name_field = 'title' + name_field = "title" - def get_rank(self): - ''' calculate how complete the data is on this edition ''' - if self.parent_work and self.parent_work.default_edition == self: + def get_rank(self, ignore_default=False): + """ calculate how complete the data is on this edition """ + if ( + not ignore_default + and self.parent_work + and self.parent_work.default_edition == self + ): # default edition has the highest rank return 20 rank = 0 @@ -204,9 +231,9 @@ class Edition(Book): return rank def save(self, *args, **kwargs): - ''' set some fields on the edition object ''' + """ set some fields on the edition object """ # calculate isbn 10/13 - if self.isbn_13 and self.isbn_13[:3] == '978' and not self.isbn_10: + if self.isbn_13 and self.isbn_13[:3] == "978" and not self.isbn_10: self.isbn_10 = isbn_13_to_10(self.isbn_13) if self.isbn_10 and not self.isbn_13: self.isbn_13 = isbn_10_to_13(self.isbn_10) @@ -218,17 +245,18 @@ class Edition(Book): def isbn_10_to_13(isbn_10): - ''' convert an isbn 10 into an isbn 13 ''' - isbn_10 = re.sub(r'[^0-9X]', '', isbn_10) + """ convert an isbn 10 into an isbn 13 """ + isbn_10 = re.sub(r"[^0-9X]", "", isbn_10) # drop the last character of the isbn 10 number (the original checkdigit) converted = isbn_10[:9] # add "978" to the front - converted = '978' + converted + converted = "978" + converted # add a check digit to the end # multiply the odd digits by 1 and the even digits by 3 and sum them try: - checksum = sum(int(i) for i in converted[::2]) + \ - sum(int(i) * 3 for i in converted[1::2]) + checksum = sum(int(i) for i in converted[::2]) + sum( + int(i) * 3 for i in converted[1::2] + ) except ValueError: return None # add the checksum mod 10 to the end @@ -239,11 +267,11 @@ def isbn_10_to_13(isbn_10): def isbn_13_to_10(isbn_13): - ''' convert isbn 13 to 10, if possible ''' - if isbn_13[:3] != '978': + """ convert isbn 13 to 10, if possible """ + if isbn_13[:3] != "978": return None - isbn_13 = re.sub(r'[^0-9X]', '', isbn_13) + isbn_13 = re.sub(r"[^0-9X]", "", isbn_13) # remove '978' and old checkdigit converted = isbn_13[3:-1] @@ -256,5 +284,5 @@ def isbn_13_to_10(isbn_13): checkdigit = checksum % 11 checkdigit = 11 - checkdigit if checkdigit == 10: - checkdigit = 'X' + checkdigit = "X" return converted + str(checkdigit) diff --git a/bookwyrm/models/connector.py b/bookwyrm/models/connector.py index c1fbf58bc..11bdbee20 100644 --- a/bookwyrm/models/connector.py +++ b/bookwyrm/models/connector.py @@ -1,21 +1,21 @@ -''' manages interfaces with external sources of book data ''' +""" manages interfaces with external sources of book data """ from django.db import models from bookwyrm.connectors.settings import CONNECTORS from .base_model import BookWyrmModel -ConnectorFiles = models.TextChoices('ConnectorFiles', CONNECTORS) +ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS) + + class Connector(BookWyrmModel): - ''' book data source connectors ''' + """ book data source connectors """ + identifier = models.CharField(max_length=255, unique=True) priority = models.IntegerField(default=2) name = models.CharField(max_length=255, null=True, blank=True) local = models.BooleanField(default=False) - connector_file = models.CharField( - max_length=255, - choices=ConnectorFiles.choices - ) + connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices) api_key = models.CharField(max_length=255, null=True, blank=True) base_url = models.CharField(max_length=255) @@ -24,7 +24,7 @@ class Connector(BookWyrmModel): search_url = models.CharField(max_length=255, null=True, blank=True) isbn_search_url = models.CharField(max_length=255, null=True, blank=True) - politeness_delay = models.IntegerField(null=True, blank=True) #seconds + politeness_delay = models.IntegerField(null=True, blank=True) # seconds max_query_count = models.IntegerField(null=True, blank=True) # how many queries executed in a unit of time, like a day query_count = models.IntegerField(default=0) @@ -32,11 +32,12 @@ class Connector(BookWyrmModel): query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True) class Meta: - ''' check that there's code to actually use this connector ''' + """ check that there's code to actually use this connector """ + constraints = [ models.CheckConstraint( check=models.Q(connector_file__in=ConnectorFiles), - name='connector_file_valid' + name="connector_file_valid", ) ] diff --git a/bookwyrm/models/favorite.py b/bookwyrm/models/favorite.py index d34cbcba8..7b72d175f 100644 --- a/bookwyrm/models/favorite.py +++ b/bookwyrm/models/favorite.py @@ -1,4 +1,4 @@ -''' like/fav/star a status ''' +""" like/fav/star a status """ from django.apps import apps from django.db import models from django.utils import timezone @@ -9,50 +9,59 @@ from .base_model import BookWyrmModel from . import fields from .status import Status + class Favorite(ActivityMixin, BookWyrmModel): - ''' fav'ing a post ''' + """ fav'ing a post """ + user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='actor') + "User", on_delete=models.PROTECT, activitypub_field="actor" + ) status = fields.ForeignKey( - 'Status', on_delete=models.PROTECT, activitypub_field='object') + "Status", on_delete=models.PROTECT, activitypub_field="object" + ) activity_serializer = activitypub.Like @classmethod def ignore_activity(cls, activity): - ''' don't bother with incoming favs of unknown statuses ''' + """ don't bother with incoming favs of unknown statuses """ return not Status.objects.filter(remote_id=activity.object).exists() def save(self, *args, **kwargs): - ''' update user active time ''' + """ update user active time """ self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) if self.status.user.local and self.status.user != self.user: notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + "bookwyrm.Notification", require_ready=True + ) notification_model.objects.create( user=self.status.user, - notification_type='FAVORITE', + notification_type="FAVORITE", related_user=self.user, - related_status=self.status + related_status=self.status, ) def delete(self, *args, **kwargs): - ''' delete and delete notifications ''' + """ delete and delete notifications """ # check for notification if self.status.user.local: notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + "bookwyrm.Notification", require_ready=True + ) notification = notification_model.objects.filter( - user=self.status.user, related_user=self.user, - related_status=self.status, notification_type='FAVORITE' + user=self.status.user, + related_user=self.user, + related_status=self.status, + notification_type="FAVORITE", ).first() if notification: notification.delete() super().delete(*args, **kwargs) class Meta: - ''' can't fav things twice ''' - unique_together = ('user', 'status') + """ can't fav things twice """ + + unique_together = ("user", "status") diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index 953cd9c8a..8f7d903e4 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -1,15 +1,17 @@ -''' connections to external ActivityPub servers ''' +""" connections to external ActivityPub servers """ from django.db import models from .base_model import BookWyrmModel class FederatedServer(BookWyrmModel): - ''' store which server's we federate with ''' + """ store which servers we federate with """ + server_name = models.CharField(max_length=255, unique=True) # federated, blocked, whatever else - status = models.CharField(max_length=255, default='federated') + status = models.CharField(max_length=255, default="federated") # is it mastodon, bookwyrm, etc application_type = models.CharField(max_length=255, null=True) application_version = models.CharField(max_length=255, null=True) + # TODO: blocked servers diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 4ea527eba..247c6aca6 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -1,4 +1,4 @@ -''' activitypub-aware django model fields ''' +""" activitypub-aware django model fields """ from dataclasses import MISSING import re from uuid import uuid4 @@ -18,37 +18,43 @@ from bookwyrm.settings import DOMAIN def validate_remote_id(value): - ''' make sure the remote_id looks like a url ''' - if not value or not re.match(r'^http.?:\/\/[^\s]+$', value): + """ make sure the remote_id looks like a url """ + if not value or not re.match(r"^http.?:\/\/[^\s]+$", value): raise ValidationError( - _('%(value)s is not a valid remote_id'), - params={'value': value}, + _("%(value)s is not a valid remote_id"), + params={"value": value}, ) def validate_localname(value): - ''' make sure localnames look okay ''' - if not re.match(r'^[A-Za-z\-_\.0-9]+$', value): + """ make sure localnames look okay """ + if not re.match(r"^[A-Za-z\-_\.0-9]+$", value): raise ValidationError( - _('%(value)s is not a valid username'), - params={'value': value}, + _("%(value)s is not a valid username"), + params={"value": value}, ) def validate_username(value): - ''' make sure usernames look okay ''' - if not re.match(r'^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$', value): + """ make sure usernames look okay """ + if not re.match(r"^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$", value): raise ValidationError( - _('%(value)s is not a valid username'), - params={'value': value}, + _("%(value)s is not a valid username"), + params={"value": value}, ) class ActivitypubFieldMixin: - ''' make a database field serializable ''' - def __init__(self, *args, \ - activitypub_field=None, activitypub_wrapper=None, - deduplication_field=False, **kwargs): + """ make a database field serializable """ + + def __init__( + self, + *args, + activitypub_field=None, + activitypub_wrapper=None, + deduplication_field=False, + **kwargs + ): self.deduplication_field = deduplication_field if activitypub_wrapper: self.activitypub_wrapper = activitypub_field @@ -57,24 +63,22 @@ class ActivitypubFieldMixin: self.activitypub_field = activitypub_field super().__init__(*args, **kwargs) - def set_field_from_activity(self, instance, data): - ''' helper function for assinging a value to the field ''' + """ helper function for assinging a value to the field """ try: value = getattr(data, self.get_activitypub_field()) except AttributeError: # masssively hack-y workaround for boosts - if self.get_activitypub_field() != 'attributedTo': + if self.get_activitypub_field() != "attributedTo": raise - value = getattr(data, 'actor') + value = getattr(data, "actor") formatted = self.field_from_activity(value) - if formatted is None or formatted is MISSING: + if formatted is None or formatted is MISSING or formatted == {}: return setattr(instance, self.name, formatted) - def set_activity_from_field(self, activity, instance): - ''' update the json object ''' + """ update the json object """ value = getattr(instance, self.name) formatted = self.field_to_activity(value) if formatted is None: @@ -82,37 +86,37 @@ class ActivitypubFieldMixin: key = self.get_activitypub_field() # TODO: surely there's a better way - if instance.__class__.__name__ == 'Boost' and key == 'attributedTo': - key = 'actor' + if instance.__class__.__name__ == "Boost" and key == "attributedTo": + key = "actor" if isinstance(activity.get(key), list): activity[key] += formatted else: activity[key] = formatted - def field_to_activity(self, value): - ''' formatter to convert a model value into activitypub ''' - if hasattr(self, 'activitypub_wrapper'): + """ formatter to convert a model value into activitypub """ + if hasattr(self, "activitypub_wrapper"): return {self.activitypub_wrapper: value} return value def field_from_activity(self, value): - ''' formatter to convert activitypub into a model value ''' - if hasattr(self, 'activitypub_wrapper'): + """ formatter to convert activitypub into a model value """ + if value and hasattr(self, "activitypub_wrapper"): value = value.get(self.activitypub_wrapper) return value def get_activitypub_field(self): - ''' model_field_name to activitypubFieldName ''' + """ model_field_name to activitypubFieldName """ if self.activitypub_field: return self.activitypub_field - name = self.name.split('.')[-1] - components = name.split('_') - return components[0] + ''.join(x.title() for x in components[1:]) + name = self.name.split(".")[-1] + components = name.split("_") + return components[0] + "".join(x.title() for x in components[1:]) class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): - ''' default (de)serialization for foreign key and one to one ''' + """ default (de)serialization for foreign key and one to one """ + def __init__(self, *args, load_remote=True, **kwargs): self.load_remote = load_remote super().__init__(*args, **kwargs) @@ -122,7 +126,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): return None related_model = self.related_model - if hasattr(value, 'id') and value.id: + if hasattr(value, "id") and value.id: if not self.load_remote: # only look in the local database return related_model.find_existing(value.serialize()) @@ -142,99 +146,98 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): class RemoteIdField(ActivitypubFieldMixin, models.CharField): - ''' a url that serves as a unique identifier ''' + """ a url that serves as a unique identifier """ + def __init__(self, *args, max_length=255, validators=None, **kwargs): validators = validators or [validate_remote_id] - super().__init__( - *args, max_length=max_length, validators=validators, - **kwargs - ) + super().__init__(*args, max_length=max_length, validators=validators, **kwargs) # for this field, the default is true. false everywhere else. - self.deduplication_field = kwargs.get('deduplication_field', True) + self.deduplication_field = kwargs.get("deduplication_field", True) class UsernameField(ActivitypubFieldMixin, models.CharField): - ''' activitypub-aware username field ''' - def __init__(self, activitypub_field='preferredUsername', **kwargs): + """ activitypub-aware username field """ + + def __init__(self, activitypub_field="preferredUsername", **kwargs): self.activitypub_field = activitypub_field # I don't totally know why pylint is mad at this, but it makes it work - super( #pylint: disable=bad-super-call - ActivitypubFieldMixin, self - ).__init__( - _('username'), + super(ActivitypubFieldMixin, self).__init__( # pylint: disable=bad-super-call + _("username"), max_length=150, unique=True, validators=[validate_username], error_messages={ - 'unique': _('A user with that username already exists.'), + "unique": _("A user with that username already exists."), }, ) def deconstruct(self): - ''' implementation of models.Field deconstruct ''' + """ implementation of models.Field deconstruct """ name, path, args, kwargs = super().deconstruct() - del kwargs['verbose_name'] - del kwargs['max_length'] - del kwargs['unique'] - del kwargs['validators'] - del kwargs['error_messages'] + del kwargs["verbose_name"] + del kwargs["max_length"] + del kwargs["unique"] + del kwargs["validators"] + del kwargs["error_messages"] return name, path, args, kwargs def field_to_activity(self, value): - return value.split('@')[0] + return value.split("@")[0] -PrivacyLevels = models.TextChoices('Privacy', [ - 'public', - 'unlisted', - 'followers', - 'direct' -]) +PrivacyLevels = models.TextChoices( + "Privacy", ["public", "unlisted", "followers", "direct"] +) + class PrivacyField(ActivitypubFieldMixin, models.CharField): - ''' this maps to two differente activitypub fields ''' - public = 'https://www.w3.org/ns/activitystreams#Public' + """ this maps to two differente activitypub fields """ + + public = "https://www.w3.org/ns/activitystreams#Public" + def __init__(self, *args, **kwargs): super().__init__( - *args, max_length=255, - choices=PrivacyLevels.choices, default='public') + *args, max_length=255, choices=PrivacyLevels.choices, default="public" + ) def set_field_from_activity(self, instance, data): to = data.to cc = data.cc if to == [self.public]: - setattr(instance, self.name, 'public') + setattr(instance, self.name, "public") elif cc == []: - setattr(instance, self.name, 'direct') + setattr(instance, self.name, "direct") elif self.public in cc: - setattr(instance, self.name, 'unlisted') + setattr(instance, self.name, "unlisted") else: - setattr(instance, self.name, 'followers') + setattr(instance, self.name, "followers") def set_activity_from_field(self, activity, instance): # explicitly to anyone mentioned (statuses only) mentions = [] - if hasattr(instance, 'mention_users'): + if hasattr(instance, "mention_users"): mentions = [u.remote_id for u in instance.mention_users.all()] # this is a link to the followers list - followers = instance.user.__class__._meta.get_field('followers')\ - .field_to_activity(instance.user.followers) - if instance.privacy == 'public': - activity['to'] = [self.public] - activity['cc'] = [followers] + mentions - elif instance.privacy == 'unlisted': - activity['to'] = [followers] - activity['cc'] = [self.public] + mentions - elif instance.privacy == 'followers': - activity['to'] = [followers] - activity['cc'] = mentions - if instance.privacy == 'direct': - activity['to'] = mentions - activity['cc'] = [] + followers = instance.user.__class__._meta.get_field( + "followers" + ).field_to_activity(instance.user.followers) + if instance.privacy == "public": + activity["to"] = [self.public] + activity["cc"] = [followers] + mentions + elif instance.privacy == "unlisted": + activity["to"] = [followers] + activity["cc"] = [self.public] + mentions + elif instance.privacy == "followers": + activity["to"] = [followers] + activity["cc"] = mentions + if instance.privacy == "direct": + activity["to"] = mentions + activity["cc"] = [] class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): - ''' activitypub-aware foreign key field ''' + """ activitypub-aware foreign key field """ + def field_to_activity(self, value): if not value: return None @@ -242,7 +245,8 @@ class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): - ''' activitypub-aware foreign key field ''' + """ activitypub-aware foreign key field """ + def field_to_activity(self, value): if not value: return None @@ -250,13 +254,14 @@ class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): - ''' activitypub-aware many to many field ''' + """ activitypub-aware many to many field """ + def __init__(self, *args, link_only=False, **kwargs): self.link_only = link_only super().__init__(*args, **kwargs) def set_field_from_activity(self, instance, data): - ''' helper function for assinging a value to the field ''' + """ helper function for assinging a value to the field """ value = getattr(data, self.get_activitypub_field()) formatted = self.field_from_activity(value) if formatted is None or formatted is MISSING: @@ -266,7 +271,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): def field_to_activity(self, value): if self.link_only: - return '%s/%s' % (value.instance.remote_id, self.name) + return "%s/%s" % (value.instance.remote_id, self.name) return [i.remote_id for i in value.all()] def field_from_activity(self, value): @@ -279,29 +284,31 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): except ValidationError: continue items.append( - activitypub.resolve_remote_id( - remote_id, model=self.related_model) + activitypub.resolve_remote_id(remote_id, model=self.related_model) ) return items class TagField(ManyToManyField): - ''' special case of many to many that uses Tags ''' + """ special case of many to many that uses Tags """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.activitypub_field = 'tag' + self.activitypub_field = "tag" def field_to_activity(self, value): tags = [] for item in value.all(): activity_type = item.__class__.__name__ - if activity_type == 'User': - activity_type = 'Mention' - tags.append(activitypub.Link( - href=item.remote_id, - name=getattr(item, item.name_field), - type=activity_type - )) + if activity_type == "User": + activity_type = "Mention" + tags.append( + activitypub.Link( + href=item.remote_id, + name=getattr(item, item.name_field), + type=activity_type, + ) + ) return tags def field_from_activity(self, value): @@ -310,38 +317,38 @@ class TagField(ManyToManyField): items = [] for link_json in value: link = activitypub.Link(**link_json) - tag_type = link.type if link.type != 'Mention' else 'Person' - if tag_type == 'Book': - tag_type = 'Edition' + tag_type = link.type if link.type != "Mention" else "Person" + if tag_type == "Book": + tag_type = "Edition" if tag_type != self.related_model.activity_serializer.type: # tags can contain multiple types continue items.append( - activitypub.resolve_remote_id( - link.href, model=self.related_model) + activitypub.resolve_remote_id(link.href, model=self.related_model) ) return items def image_serializer(value, alt): - ''' helper for serializing images ''' - if value and hasattr(value, 'url'): + """ helper for serializing images """ + if value and hasattr(value, "url"): url = value.url else: return None - url = 'https://%s%s' % (DOMAIN, url) + url = "https://%s%s" % (DOMAIN, url) return activitypub.Image(url=url, name=alt) class ImageField(ActivitypubFieldMixin, models.ImageField): - ''' activitypub-aware image field ''' + """ activitypub-aware image field """ + def __init__(self, *args, alt_field=None, **kwargs): self.alt_field = alt_field super().__init__(*args, **kwargs) # pylint: disable=arguments-differ def set_field_from_activity(self, instance, data, save=True): - ''' helper function for assinging a value to the field ''' + """ helper function for assinging a value to the field """ value = getattr(data, self.get_activitypub_field()) formatted = self.field_from_activity(value) if formatted is None or formatted is MISSING: @@ -358,16 +365,14 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): key = self.get_activitypub_field() activity[key] = formatted - def field_to_activity(self, value, alt=None): return image_serializer(value, alt) - def field_from_activity(self, value): image_slug = value # when it's an inline image (User avatar/icon, Book cover), it's a json # blob, but when it's an attached image, it's just a url - if hasattr(image_slug, 'url'): + if hasattr(image_slug, "url"): url = image_slug.url elif isinstance(image_slug, str): url = image_slug @@ -383,13 +388,14 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): if not response: return None - image_name = str(uuid4()) + '.' + url.split('.')[-1] + image_name = str(uuid4()) + "." + url.split(".")[-1] image_content = ContentFile(response.content) return [image_name, image_content] class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): - ''' activitypub-aware datetime field ''' + """ activitypub-aware datetime field """ + def field_to_activity(self, value): if not value: return None @@ -405,8 +411,10 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): except (ParserError, TypeError): return None + class HtmlField(ActivitypubFieldMixin, models.TextField): - ''' a text field for storing html ''' + """ a text field for storing html """ + def field_from_activity(self, value): if not value or value == MISSING: return None @@ -414,19 +422,29 @@ class HtmlField(ActivitypubFieldMixin, models.TextField): sanitizer.feed(value) return sanitizer.get_output() + class ArrayField(ActivitypubFieldMixin, DjangoArrayField): - ''' activitypub-aware array field ''' + """ activitypub-aware array field """ + def field_to_activity(self, value): return [str(i) for i in value] + class CharField(ActivitypubFieldMixin, models.CharField): - ''' activitypub-aware char field ''' + """ activitypub-aware char field """ + class TextField(ActivitypubFieldMixin, models.TextField): - ''' activitypub-aware text field ''' + """ activitypub-aware text field """ + class BooleanField(ActivitypubFieldMixin, models.BooleanField): - ''' activitypub-aware boolean field ''' + """ activitypub-aware boolean field """ + class IntegerField(ActivitypubFieldMixin, models.IntegerField): - ''' activitypub-aware boolean field ''' + """ activitypub-aware boolean field """ + + +class DecimalField(ActivitypubFieldMixin, models.DecimalField): + """ activitypub-aware boolean field """ diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index ca05ddb08..026cf7cd5 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -1,9 +1,8 @@ -''' track progress of goodreads imports ''' +""" track progress of goodreads imports """ import re import dateutil.parser from django.apps import apps -from django.contrib.postgres.fields import JSONField from django.db import models from django.utils import timezone @@ -14,13 +13,14 @@ from .fields import PrivacyLevels # Mapping goodreads -> bookwyrm shelf titles. GOODREADS_SHELVES = { - 'read': 'read', - 'currently-reading': 'reading', - 'to-read': 'to-read', + "read": "read", + "currently-reading": "reading", + "to-read": "to-read", } + def unquote_string(text): - ''' resolve csv quote weirdness ''' + """ resolve csv quote weirdness """ match = re.match(r'="([^"]*)"', text) if match: return match.group(1) @@ -28,63 +28,57 @@ def unquote_string(text): def construct_search_term(title, author): - ''' formulate a query for the data connector ''' + """ formulate a query for the data connector """ # Strip brackets (usually series title from search term) - title = re.sub(r'\s*\([^)]*\)\s*', '', title) + title = re.sub(r"\s*\([^)]*\)\s*", "", title) # Open library doesn't like including author initials in search term. - author = re.sub(r'(\w\.)+\s*', '', author) + author = re.sub(r"(\w\.)+\s*", "", author) - return ' '.join([title, author]) + return " ".join([title, author]) class ImportJob(models.Model): - ''' entry for a specific request for book data import ''' + """ entry for a specific request for book data import """ + user = models.ForeignKey(User, on_delete=models.CASCADE) created_date = models.DateTimeField(default=timezone.now) task_id = models.CharField(max_length=100, null=True) include_reviews = models.BooleanField(default=True) complete = models.BooleanField(default=False) privacy = models.CharField( - max_length=255, - default='public', - choices=PrivacyLevels.choices + max_length=255, default="public", choices=PrivacyLevels.choices ) retry = models.BooleanField(default=False) def save(self, *args, **kwargs): - ''' save and notify ''' + """ save and notify """ super().save(*args, **kwargs) if self.complete: notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + "bookwyrm.Notification", require_ready=True + ) notification_model.objects.create( user=self.user, - notification_type='IMPORT', + notification_type="IMPORT", related_import=self, ) class ImportItem(models.Model): - ''' a single line of a csv being imported ''' - job = models.ForeignKey( - ImportJob, - on_delete=models.CASCADE, - related_name='items') + """ a single line of a csv being imported """ + + job = models.ForeignKey(ImportJob, on_delete=models.CASCADE, related_name="items") index = models.IntegerField() - data = JSONField() - book = models.ForeignKey( - Book, on_delete=models.SET_NULL, null=True, blank=True) + data = models.JSONField() + book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True) fail_reason = models.TextField(null=True) def resolve(self): - ''' try various ways to lookup a book ''' - self.book = ( - self.get_book_from_isbn() or - self.get_book_from_title_author() - ) + """ try various ways to lookup a book """ + self.book = self.get_book_from_isbn() or self.get_book_from_title_author() def get_book_from_isbn(self): - ''' search by isbn ''' + """ search by isbn """ search_result = connector_manager.first_search_result( self.isbn, min_confidence=0.999 ) @@ -93,13 +87,9 @@ class ImportItem(models.Model): return search_result.connector.get_or_create_book(search_result.key) return None - def get_book_from_title_author(self): - ''' search by title and author ''' - search_term = construct_search_term( - self.title, - self.author - ) + """ search by title and author """ + search_term = construct_search_term(self.title, self.author) search_result = connector_manager.first_search_result( search_term, min_confidence=0.999 ) @@ -108,84 +98,85 @@ class ImportItem(models.Model): return search_result.connector.get_or_create_book(search_result.key) return None - @property def title(self): - ''' get the book title ''' - return self.data['Title'] + """ get the book title """ + return self.data["Title"] @property def author(self): - ''' get the book title ''' - return self.data['Author'] + """ get the book title """ + return self.data["Author"] @property def isbn(self): - ''' pulls out the isbn13 field from the csv line data ''' - return unquote_string(self.data['ISBN13']) + """ pulls out the isbn13 field from the csv line data """ + return unquote_string(self.data["ISBN13"]) @property def shelf(self): - ''' the goodreads shelf field ''' - if self.data['Exclusive Shelf']: - return GOODREADS_SHELVES.get(self.data['Exclusive Shelf']) + """ the goodreads shelf field """ + if self.data["Exclusive Shelf"]: + return GOODREADS_SHELVES.get(self.data["Exclusive Shelf"]) return None @property def review(self): - ''' a user-written review, to be imported with the book data ''' - return self.data['My Review'] + """ a user-written review, to be imported with the book data """ + return self.data["My Review"] @property def rating(self): - ''' x/5 star rating for a book ''' - return int(self.data['My Rating']) + """ x/5 star rating for a book """ + return int(self.data["My Rating"]) @property def date_added(self): - ''' when the book was added to this dataset ''' - if self.data['Date Added']: - return timezone.make_aware( - dateutil.parser.parse(self.data['Date Added'])) + """ when the book was added to this dataset """ + if self.data["Date Added"]: + return timezone.make_aware(dateutil.parser.parse(self.data["Date Added"])) return None @property def date_started(self): - ''' when the book was started ''' - if "Date Started" in self.data and self.data['Date Started']: - return timezone.make_aware( - dateutil.parser.parse(self.data['Date Started'])) + """ when the book was started """ + if "Date Started" in self.data and self.data["Date Started"]: + return timezone.make_aware(dateutil.parser.parse(self.data["Date Started"])) return None @property def date_read(self): - ''' the date a book was completed ''' - if self.data['Date Read']: - return timezone.make_aware( - dateutil.parser.parse(self.data['Date Read'])) + """ the date a book was completed """ + if self.data["Date Read"]: + return timezone.make_aware(dateutil.parser.parse(self.data["Date Read"])) return None @property def reads(self): - ''' formats a read through dataset for the book in this line ''' + """ formats a read through dataset for the book in this line """ start_date = self.date_started # Goodreads special case (no 'date started' field) - if ((self.shelf == 'reading' or (self.shelf == 'read' and self.date_read)) - and self.date_added and not start_date): + if ( + (self.shelf == "reading" or (self.shelf == "read" and self.date_read)) + and self.date_added + and not start_date + ): start_date = self.date_added - if (start_date and start_date is not None and not self.date_read): + if start_date and start_date is not None and not self.date_read: return [ReadThrough(start_date=start_date)] if self.date_read: - return [ReadThrough( - start_date=start_date, - finish_date=self.date_read, - )] + return [ + ReadThrough( + start_date=start_date, + finish_date=self.date_read, + ) + ] return [] def __repr__(self): - return "<{!r}Item {!r}>".format(self.data['import_source'], self.data['Title']) + return "<{!r}Item {!r}>".format(self.data["import_source"], self.data["Title"]) def __str__(self): - return "{} by {}".format(self.data['Title'], self.data['Author']) + return "{} by {}".format(self.data["Title"], self.data["Author"]) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 1b14c2aa5..a05325f3f 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -1,4 +1,4 @@ -''' make a list of books!! ''' +""" make a list of books!! """ from django.apps import apps from django.db import models @@ -9,86 +9,89 @@ from .base_model import BookWyrmModel from . import fields -CurationType = models.TextChoices('Curation', [ - 'closed', - 'open', - 'curated', -]) +CurationType = models.TextChoices( + "Curation", + [ + "closed", + "open", + "curated", + ], +) + class List(OrderedCollectionMixin, BookWyrmModel): - ''' a list of books ''' + """ a list of books """ + name = fields.CharField(max_length=100) user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='owner') - description = fields.TextField( - blank=True, null=True, activitypub_field='summary') + "User", on_delete=models.PROTECT, activitypub_field="owner" + ) + description = fields.TextField(blank=True, null=True, activitypub_field="summary") privacy = fields.PrivacyField() curation = fields.CharField( - max_length=255, - default='closed', - choices=CurationType.choices + max_length=255, default="closed", choices=CurationType.choices ) books = models.ManyToManyField( - 'Edition', + "Edition", symmetrical=False, - through='ListItem', - through_fields=('book_list', 'book'), + through="ListItem", + through_fields=("book_list", "book"), ) activity_serializer = activitypub.BookList def get_remote_id(self): - ''' don't want the user to be in there in this case ''' - return 'https://%s/list/%d' % (DOMAIN, self.id) + """ don't want the user to be in there in this case """ + return "https://%s/list/%d" % (DOMAIN, self.id) @property def collection_queryset(self): - ''' list of books for this shelf, overrides OrderedCollectionMixin ''' - return self.books.filter( - listitem__approved=True - ).all().order_by('listitem') + """ list of books for this shelf, overrides OrderedCollectionMixin """ + return self.books.filter(listitem__approved=True).all().order_by("listitem") class Meta: - ''' default sorting ''' - ordering = ('-updated_date',) + """ default sorting """ + + ordering = ("-updated_date",) class ListItem(CollectionItemMixin, BookWyrmModel): - ''' ok ''' + """ ok """ + book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='object') + "Edition", on_delete=models.PROTECT, activitypub_field="object" + ) book_list = fields.ForeignKey( - 'List', on_delete=models.CASCADE, activitypub_field='target') + "List", on_delete=models.CASCADE, activitypub_field="target" + ) user = fields.ForeignKey( - 'User', - on_delete=models.PROTECT, - activitypub_field='actor' + "User", on_delete=models.PROTECT, activitypub_field="actor" ) notes = fields.TextField(blank=True, null=True) approved = models.BooleanField(default=True) order = fields.IntegerField(blank=True, null=True) - endorsement = models.ManyToManyField('User', related_name='endorsers') + endorsement = models.ManyToManyField("User", related_name="endorsers") activity_serializer = activitypub.Add - object_field = 'book' - collection_field = 'book_list' + object_field = "book" + collection_field = "book_list" def save(self, *args, **kwargs): - ''' create a notification too ''' + """ create a notification too """ created = not bool(self.id) super().save(*args, **kwargs) list_owner = self.book_list.user # create a notification if somoene ELSE added to a local user's list if created and list_owner.local and list_owner != self.user: - model = apps.get_model('bookwyrm.Notification', require_ready=True) + model = apps.get_model("bookwyrm.Notification", require_ready=True) model.objects.create( user=list_owner, related_user=self.user, related_list_item=self, - notification_type='ADD', + notification_type="ADD", ) - class Meta: - ''' an opinionated constraint! you can't put a book on a list twice ''' - unique_together = ('book', 'book_list') - ordering = ('-created_date',) + """ an opinionated constraint! you can't put a book on a list twice """ + + unique_together = ("book", "book_list") + ordering = ("-created_date",) diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 0470b3258..233d635b8 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -1,47 +1,52 @@ -''' alert a user to activity ''' +""" alert a user to activity """ from django.db import models from .base_model import BookWyrmModel NotificationType = models.TextChoices( - 'NotificationType', - 'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD') + "NotificationType", + "FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD REPORT", +) + class Notification(BookWyrmModel): - ''' you've been tagged, liked, followed, etc ''' - user = models.ForeignKey('User', on_delete=models.CASCADE) - related_book = models.ForeignKey( - 'Edition', on_delete=models.CASCADE, null=True) + """ you've been tagged, liked, followed, etc """ + + user = models.ForeignKey("User", on_delete=models.CASCADE) + related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True) related_user = models.ForeignKey( - 'User', - on_delete=models.CASCADE, null=True, related_name='related_user') - related_status = models.ForeignKey( - 'Status', on_delete=models.CASCADE, null=True) - related_import = models.ForeignKey( - 'ImportJob', on_delete=models.CASCADE, null=True) + "User", on_delete=models.CASCADE, null=True, related_name="related_user" + ) + related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True) + related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True) related_list_item = models.ForeignKey( - 'ListItem', on_delete=models.CASCADE, null=True) + "ListItem", on_delete=models.CASCADE, null=True + ) + related_report = models.ForeignKey("Report", on_delete=models.CASCADE, null=True) read = models.BooleanField(default=False) notification_type = models.CharField( - max_length=255, choices=NotificationType.choices) + max_length=255, choices=NotificationType.choices + ) def save(self, *args, **kwargs): - ''' save, but don't make dupes ''' + """ save, but don't make dupes """ # there's probably a better way to do this if self.__class__.objects.filter( - user=self.user, - related_book=self.related_book, - related_user=self.related_user, - related_status=self.related_status, - related_import=self.related_import, - related_list_item=self.related_list_item, - notification_type=self.notification_type, - ).exists(): + user=self.user, + related_book=self.related_book, + related_user=self.related_user, + related_status=self.related_status, + related_import=self.related_import, + related_list_item=self.related_list_item, + related_report=self.related_report, + notification_type=self.notification_type, + ).exists(): return super().save(*args, **kwargs) class Meta: - ''' checks if notifcation is in enum list for valid types ''' + """ checks if notifcation is in enum list for valid types """ + constraints = [ models.CheckConstraint( check=models.Q(notification_type__in=NotificationType.values), diff --git a/bookwyrm/models/readthrough.py b/bookwyrm/models/readthrough.py index 2bec3a818..3445573c4 100644 --- a/bookwyrm/models/readthrough.py +++ b/bookwyrm/models/readthrough.py @@ -1,35 +1,32 @@ -''' progress in a book ''' +""" progress in a book """ from django.db import models from django.utils import timezone from django.core import validators from .base_model import BookWyrmModel + class ProgressMode(models.TextChoices): - PAGE = 'PG', 'page' - PERCENT = 'PCT', 'percent' + PAGE = "PG", "page" + PERCENT = "PCT", "percent" + class ReadThrough(BookWyrmModel): - ''' Store a read through a book in the database. ''' - user = models.ForeignKey('User', on_delete=models.PROTECT) - book = models.ForeignKey('Edition', on_delete=models.PROTECT) + """ Store a read through a book in the database. """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + book = models.ForeignKey("Edition", on_delete=models.PROTECT) progress = models.IntegerField( - validators=[validators.MinValueValidator(0)], - null=True, - blank=True) + validators=[validators.MinValueValidator(0)], null=True, blank=True + ) progress_mode = models.CharField( - max_length=3, - choices=ProgressMode.choices, - default=ProgressMode.PAGE) - start_date = models.DateTimeField( - blank=True, - null=True) - finish_date = models.DateTimeField( - blank=True, - null=True) + max_length=3, choices=ProgressMode.choices, default=ProgressMode.PAGE + ) + start_date = models.DateTimeField(blank=True, null=True) + finish_date = models.DateTimeField(blank=True, null=True) def save(self, *args, **kwargs): - ''' update user active time ''' + """ update user active time """ self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) @@ -37,22 +34,22 @@ class ReadThrough(BookWyrmModel): def create_update(self): if self.progress: return self.progressupdate_set.create( - user=self.user, - progress=self.progress, - mode=self.progress_mode) + user=self.user, progress=self.progress, mode=self.progress_mode + ) + class ProgressUpdate(BookWyrmModel): - ''' Store progress through a book in the database. ''' - user = models.ForeignKey('User', on_delete=models.PROTECT) - readthrough = models.ForeignKey('ReadThrough', on_delete=models.CASCADE) + """ Store progress through a book in the database. """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + readthrough = models.ForeignKey("ReadThrough", on_delete=models.CASCADE) progress = models.IntegerField(validators=[validators.MinValueValidator(0)]) mode = models.CharField( - max_length=3, - choices=ProgressMode.choices, - default=ProgressMode.PAGE) + max_length=3, choices=ProgressMode.choices, default=ProgressMode.PAGE + ) def save(self, *args, **kwargs): - ''' update user active time ''' + """ update user active time """ self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 3b0e85d41..df99d2165 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -1,4 +1,4 @@ -''' defines relationships between users ''' +""" defines relationships between users """ from django.apps import apps from django.db import models, transaction, IntegrityError from django.db.models import Q @@ -11,71 +11,74 @@ from . import fields class UserRelationship(BookWyrmModel): - ''' many-to-many through table for followers ''' + """ many-to-many through table for followers """ + user_subject = fields.ForeignKey( - 'User', + "User", on_delete=models.PROTECT, - related_name='%(class)s_user_subject', - activitypub_field='actor', + related_name="%(class)s_user_subject", + activitypub_field="actor", ) user_object = fields.ForeignKey( - 'User', + "User", on_delete=models.PROTECT, - related_name='%(class)s_user_object', - activitypub_field='object', + related_name="%(class)s_user_object", + activitypub_field="object", ) @property def privacy(self): - ''' all relationships are handled directly with the participants ''' - return 'direct' + """ all relationships are handled directly with the participants """ + return "direct" @property def recipients(self): - ''' the remote user needs to recieve direct broadcasts ''' + """ the remote user needs to recieve direct broadcasts """ return [u for u in [self.user_subject, self.user_object] if not u.local] class Meta: - ''' relationships should be unique ''' + """ relationships should be unique """ + abstract = True constraints = [ models.UniqueConstraint( - fields=['user_subject', 'user_object'], - name='%(class)s_unique' + fields=["user_subject", "user_object"], name="%(class)s_unique" ), models.CheckConstraint( - check=~models.Q(user_subject=models.F('user_object')), - name='%(class)s_no_self' - ) + check=~models.Q(user_subject=models.F("user_object")), + name="%(class)s_no_self", + ), ] - def get_remote_id(self, status=None):# pylint: disable=arguments-differ - ''' use shelf identifier in remote_id ''' - status = status or 'follows' + def get_remote_id(self, status=None): # pylint: disable=arguments-differ + """ use shelf identifier in remote_id """ + status = status or "follows" base_path = self.user_subject.remote_id - return '%s#%s/%d' % (base_path, status, self.id) + return "%s#%s/%d" % (base_path, status, self.id) class UserFollows(ActivityMixin, UserRelationship): - ''' Following a user ''' - status = 'follows' + """ Following a user """ + + status = "follows" def to_activity(self): - ''' overrides default to manually set serializer ''' + """ overrides default to manually set serializer """ return activitypub.Follow(**generate_activity(self)) def save(self, *args, **kwargs): - ''' really really don't let a user follow someone who blocked them ''' + """ really really don't let a user follow someone who blocked them """ # blocking in either direction is a no-go if UserBlocks.objects.filter( - Q( - user_subject=self.user_subject, - user_object=self.user_object, - ) | Q( - user_subject=self.user_object, - user_object=self.user_subject, - ) - ).exists(): + Q( + user_subject=self.user_subject, + user_object=self.user_object, + ) + | Q( + user_subject=self.user_object, + user_object=self.user_subject, + ) + ).exists(): raise IntegrityError() # don't broadcast this type of relationship -- accepts and requests # are handled by the UserFollowRequest model @@ -83,7 +86,7 @@ class UserFollows(ActivityMixin, UserRelationship): @classmethod def from_request(cls, follow_request): - ''' converts a follow request into a follow relationship ''' + """ converts a follow request into a follow relationship """ return cls.objects.create( user_subject=follow_request.user_subject, user_object=follow_request.user_object, @@ -92,28 +95,30 @@ class UserFollows(ActivityMixin, UserRelationship): class UserFollowRequest(ActivitypubMixin, UserRelationship): - ''' following a user requires manual or automatic confirmation ''' - status = 'follow_request' + """ following a user requires manual or automatic confirmation """ + + status = "follow_request" activity_serializer = activitypub.Follow def save(self, *args, broadcast=True, **kwargs): - ''' make sure the follow or block relationship doesn't already exist ''' + """ make sure the follow or block relationship doesn't already exist """ # don't create a request if a follow already exists if UserFollows.objects.filter( - user_subject=self.user_subject, - user_object=self.user_object, - ).exists(): + user_subject=self.user_subject, + user_object=self.user_object, + ).exists(): raise IntegrityError() # blocking in either direction is a no-go if UserBlocks.objects.filter( - Q( - user_subject=self.user_subject, - user_object=self.user_object, - ) | Q( - user_subject=self.user_object, - user_object=self.user_subject, - ) - ).exists(): + Q( + user_subject=self.user_subject, + user_object=self.user_object, + ) + | Q( + user_subject=self.user_object, + user_object=self.user_subject, + ) + ).exists(): raise IntegrityError() super().save(*args, **kwargs) @@ -125,39 +130,35 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): if not manually_approves: self.accept() - model = apps.get_model('bookwyrm.Notification', require_ready=True) - notification_type = 'FOLLOW_REQUEST' if \ - manually_approves else 'FOLLOW' + model = apps.get_model("bookwyrm.Notification", require_ready=True) + notification_type = "FOLLOW_REQUEST" if manually_approves else "FOLLOW" model.objects.create( user=self.user_object, related_user=self.user_subject, notification_type=notification_type, ) - def accept(self): - ''' turn this request into the real deal''' + """ turn this request into the real deal""" user = self.user_object if not self.user_subject.local: activity = activitypub.Accept( - id=self.get_remote_id(status='accepts'), + id=self.get_remote_id(status="accepts"), actor=self.user_object.remote_id, - object=self.to_activity() + object=self.to_activity(), ).serialize() self.broadcast(activity, user) with transaction.atomic(): UserFollows.from_request(self) self.delete() - - def reject(self): - ''' generate a Reject for this follow request ''' + """ generate a Reject for this follow request """ if self.user_object.local: activity = activitypub.Reject( - id=self.get_remote_id(status='rejects'), + id=self.get_remote_id(status="rejects"), actor=self.user_object.remote_id, - object=self.to_activity() + object=self.to_activity(), ).serialize() self.broadcast(activity, self.user_object) @@ -165,19 +166,20 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): class UserBlocks(ActivityMixin, UserRelationship): - ''' prevent another user from following you and seeing your posts ''' - status = 'blocks' + """ prevent another user from following you and seeing your posts """ + + status = "blocks" activity_serializer = activitypub.Block def save(self, *args, **kwargs): - ''' remove follow or follow request rels after a block is created ''' + """ remove follow or follow request rels after a block is created """ super().save(*args, **kwargs) UserFollows.objects.filter( - Q(user_subject=self.user_subject, user_object=self.user_object) | \ - Q(user_subject=self.user_object, user_object=self.user_subject) + Q(user_subject=self.user_subject, user_object=self.user_object) + | Q(user_subject=self.user_object, user_object=self.user_subject) ).delete() UserFollowRequest.objects.filter( - Q(user_subject=self.user_subject, user_object=self.user_object) | \ - Q(user_subject=self.user_object, user_object=self.user_subject) + Q(user_subject=self.user_subject, user_object=self.user_object) + | Q(user_subject=self.user_object, user_object=self.user_subject) ).delete() diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py new file mode 100644 index 000000000..f9e8905bf --- /dev/null +++ b/bookwyrm/models/report.py @@ -0,0 +1,55 @@ +""" flagged for moderation """ +from django.apps import apps +from django.db import models +from django.db.models import F, Q +from .base_model import BookWyrmModel + + +class Report(BookWyrmModel): + """ reported status or user """ + + reporter = models.ForeignKey( + "User", related_name="reporter", on_delete=models.PROTECT + ) + note = models.TextField(null=True, blank=True) + user = models.ForeignKey("User", on_delete=models.PROTECT) + statuses = models.ManyToManyField("Status", blank=True) + resolved = models.BooleanField(default=False) + + def save(self, *args, **kwargs): + """ notify admins when a report is created """ + super().save(*args, **kwargs) + user_model = apps.get_model("bookwyrm.User", require_ready=True) + # moderators and superusers should be notified + admins = user_model.objects.filter( + Q(user_permissions__name__in=["moderate_user", "moderate_post"]) + | Q(is_superuser=True) + ).all() + notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) + for admin in admins: + notification_model.objects.create( + user=admin, + related_report=self, + notification_type="REPORT", + ) + + class Meta: + """ don't let users report themselves """ + + constraints = [ + models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report") + ] + ordering = ("-created_date",) + + +class ReportComment(BookWyrmModel): + """ updates on a report """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + note = models.TextField() + report = models.ForeignKey(Report, on_delete=models.PROTECT) + + class Meta: + """ sort comments """ + + ordering = ("-created_date",) diff --git a/bookwyrm/models/shelf.py b/bookwyrm/models/shelf.py index dfb8b9b31..3185b47f7 100644 --- a/bookwyrm/models/shelf.py +++ b/bookwyrm/models/shelf.py @@ -1,4 +1,4 @@ -''' puttin' books on shelves ''' +""" puttin' books on shelves """ import re from django.db import models @@ -9,61 +9,73 @@ from . import fields class Shelf(OrderedCollectionMixin, BookWyrmModel): - ''' a list of books owned by a user ''' + """ a list of books owned by a user """ + name = fields.CharField(max_length=100) identifier = models.CharField(max_length=100) user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='owner') + "User", on_delete=models.PROTECT, activitypub_field="owner" + ) editable = models.BooleanField(default=True) privacy = fields.PrivacyField() books = models.ManyToManyField( - 'Edition', + "Edition", symmetrical=False, - through='ShelfBook', - through_fields=('shelf', 'book') + through="ShelfBook", + through_fields=("shelf", "book"), ) activity_serializer = activitypub.Shelf def save(self, *args, **kwargs): - ''' set the identifier ''' + """ set the identifier """ super().save(*args, **kwargs) if not self.identifier: - slug = re.sub(r'[^\w]', '', self.name).lower() - self.identifier = '%s-%d' % (slug, self.id) + slug = re.sub(r"[^\w]", "", self.name).lower() + self.identifier = "%s-%d" % (slug, self.id) super().save(*args, **kwargs) @property def collection_queryset(self): - ''' list of books for this shelf, overrides OrderedCollectionMixin ''' - return self.books.all().order_by('shelfbook') + """ list of books for this shelf, overrides OrderedCollectionMixin """ + return self.books.all().order_by("shelfbook") def get_remote_id(self): - ''' shelf identifier instead of id ''' + """ shelf identifier instead of id """ base_path = self.user.remote_id - return '%s/shelf/%s' % (base_path, self.identifier) + return "%s/shelf/%s" % (base_path, self.identifier) class Meta: - ''' user/shelf unqiueness ''' - unique_together = ('user', 'identifier') + """ user/shelf unqiueness """ + + unique_together = ("user", "identifier") class ShelfBook(CollectionItemMixin, BookWyrmModel): - ''' many to many join table for books and shelves ''' + """ many to many join table for books and shelves """ + book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='object') + "Edition", on_delete=models.PROTECT, activitypub_field="object" + ) shelf = fields.ForeignKey( - 'Shelf', on_delete=models.PROTECT, activitypub_field='target') + "Shelf", on_delete=models.PROTECT, activitypub_field="target" + ) user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='actor') + "User", on_delete=models.PROTECT, activitypub_field="actor" + ) activity_serializer = activitypub.Add - object_field = 'book' - collection_field = 'shelf' + object_field = "book" + collection_field = "shelf" + def save(self, *args, **kwargs): + if not self.user: + self.user = self.shelf.user + super().save(*args, **kwargs) class Meta: - ''' an opinionated constraint! - you can't put a book on shelf twice ''' - unique_together = ('book', 'shelf') - ordering = ('-created_date',) + """an opinionated constraint! + you can't put a book on shelf twice""" + + unique_together = ("book", "shelf") + ordering = ("-created_date",) diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index d39718b30..7fde6781e 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -1,4 +1,4 @@ -''' the particulars for this instance of BookWyrm ''' +""" the particulars for this instance of BookWyrm """ import base64 import datetime @@ -9,36 +9,31 @@ from django.utils import timezone from bookwyrm.settings import DOMAIN from .user import User + class SiteSettings(models.Model): - ''' customized settings for this instance ''' - name = models.CharField(default='BookWyrm', max_length=100) + """ customized settings for this instance """ + + name = models.CharField(default="BookWyrm", max_length=100) instance_tagline = models.CharField( - max_length=150, default='Social Reading and Reviewing') - instance_description = models.TextField( - default='This instance has no description.') + max_length=150, default="Social Reading and Reviewing" + ) + instance_description = models.TextField(default="This instance has no description.") registration_closed_text = models.TextField( - default='Contact an administrator to get an invite') - code_of_conduct = models.TextField( - default='Add a code of conduct here.') - privacy_policy = models.TextField( - default='Add a privacy policy here.') + default="Contact an administrator to get an invite" + ) + code_of_conduct = models.TextField(default="Add a code of conduct here.") + privacy_policy = models.TextField(default="Add a privacy policy here.") allow_registration = models.BooleanField(default=True) - logo = models.ImageField( - upload_to='logos/', null=True, blank=True - ) - logo_small = models.ImageField( - upload_to='logos/', null=True, blank=True - ) - favicon = models.ImageField( - upload_to='logos/', null=True, blank=True - ) + logo = models.ImageField(upload_to="logos/", null=True, blank=True) + logo_small = models.ImageField(upload_to="logos/", null=True, blank=True) + favicon = models.ImageField(upload_to="logos/", null=True, blank=True) support_link = models.CharField(max_length=255, null=True, blank=True) support_title = models.CharField(max_length=100, null=True, blank=True) admin_email = models.EmailField(max_length=255, null=True, blank=True) @classmethod def get(cls): - ''' gets the site settings db entry or defaults ''' + """ gets the site settings db entry or defaults """ try: return cls.objects.get(id=1) except cls.DoesNotExist: @@ -46,12 +41,15 @@ class SiteSettings(models.Model): default_settings.save() return default_settings + def new_access_code(): - ''' the identifier for a user invite ''' - return base64.b32encode(Random.get_random_bytes(5)).decode('ascii') + """ the identifier for a user invite """ + return base64.b32encode(Random.get_random_bytes(5)).decode("ascii") + class SiteInvite(models.Model): - ''' gives someone access to create an account on the instance ''' + """ gives someone access to create an account on the instance """ + created_date = models.DateTimeField(auto_now_add=True) code = models.CharField(max_length=32, default=new_access_code) expiry = models.DateTimeField(blank=True, null=True) @@ -60,34 +58,35 @@ class SiteInvite(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) def valid(self): - ''' make sure it hasn't expired or been used ''' - return ( - (self.expiry is None or self.expiry > timezone.now()) and - (self.use_limit is None or self.times_used < self.use_limit)) + """ make sure it hasn't expired or been used """ + return (self.expiry is None or self.expiry > timezone.now()) and ( + self.use_limit is None or self.times_used < self.use_limit + ) @property def link(self): - ''' formats the invite link ''' - return 'https://{}/invite/{}'.format(DOMAIN, self.code) + """ formats the invite link """ + return "https://{}/invite/{}".format(DOMAIN, self.code) def get_passowrd_reset_expiry(): - ''' give people a limited time to use the link ''' + """ give people a limited time to use the link """ now = timezone.now() return now + datetime.timedelta(days=1) class PasswordReset(models.Model): - ''' gives someone access to create an account on the instance ''' + """ gives someone access to create an account on the instance """ + code = models.CharField(max_length=32, default=new_access_code) expiry = models.DateTimeField(default=get_passowrd_reset_expiry) user = models.OneToOneField(User, on_delete=models.CASCADE) def valid(self): - ''' make sure it hasn't expired or been used ''' + """ make sure it hasn't expired or been used """ return self.expiry > timezone.now() @property def link(self): - ''' formats the invite link ''' - return 'https://{}/password-reset/{}'.format(DOMAIN, self.code) + """ formats the invite link """ + return "https://{}/password-reset/{}".format(DOMAIN, self.code) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index ba9727f58..09a7c4ec1 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -1,10 +1,11 @@ -''' models for storing different kinds of Activities ''' +""" models for storing different kinds of Activities """ from dataclasses import MISSING import re from django.apps import apps from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.template.loader import get_template from django.utils import timezone from model_utils.managers import InheritanceManager @@ -17,76 +18,81 @@ from . import fields class Status(OrderedCollectionPageMixin, BookWyrmModel): - ''' any post, like a reply to a review, etc ''' + """ any post, like a reply to a review, etc """ + user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='attributedTo') + "User", on_delete=models.PROTECT, activitypub_field="attributedTo" + ) content = fields.HtmlField(blank=True, null=True) - mention_users = fields.TagField('User', related_name='mention_user') - mention_books = fields.TagField('Edition', related_name='mention_book') + mention_users = fields.TagField("User", related_name="mention_user") + mention_books = fields.TagField("Edition", related_name="mention_book") local = models.BooleanField(default=True) content_warning = fields.CharField( - max_length=500, blank=True, null=True, activitypub_field='summary') + max_length=500, blank=True, null=True, activitypub_field="summary" + ) privacy = fields.PrivacyField(max_length=255) sensitive = fields.BooleanField(default=False) # created date is different than publish date because of federated posts published_date = fields.DateTimeField( - default=timezone.now, activitypub_field='published') + default=timezone.now, activitypub_field="published" + ) deleted = models.BooleanField(default=False) deleted_date = models.DateTimeField(blank=True, null=True) favorites = models.ManyToManyField( - 'User', + "User", symmetrical=False, - through='Favorite', - through_fields=('status', 'user'), - related_name='user_favorites' + through="Favorite", + through_fields=("status", "user"), + related_name="user_favorites", ) reply_parent = fields.ForeignKey( - 'self', + "self", null=True, on_delete=models.PROTECT, - activitypub_field='inReplyTo', + activitypub_field="inReplyTo", ) objects = InheritanceManager() activity_serializer = activitypub.Note - serialize_reverse_fields = [('attachments', 'attachment', 'id')] - deserialize_reverse_fields = [('attachments', 'attachment')] - + serialize_reverse_fields = [("attachments", "attachment", "id")] + deserialize_reverse_fields = [("attachments", "attachment")] def save(self, *args, **kwargs): - ''' save and notify ''' + """ save and notify """ super().save(*args, **kwargs) - notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) if self.deleted: notification_model.objects.filter(related_status=self).delete() - if self.reply_parent and self.reply_parent.user != self.user and \ - self.reply_parent.user.local: + if ( + self.reply_parent + and self.reply_parent.user != self.user + and self.reply_parent.user.local + ): notification_model.objects.create( user=self.reply_parent.user, - notification_type='REPLY', + notification_type="REPLY", related_user=self.user, related_status=self, ) for mention_user in self.mention_users.all(): # avoid double-notifying about this status - if not mention_user.local or \ - (self.reply_parent and \ - mention_user == self.reply_parent.user): + if not mention_user.local or ( + self.reply_parent and mention_user == self.reply_parent.user + ): continue notification_model.objects.create( user=mention_user, - notification_type='MENTION', + notification_type="MENTION", related_user=self.user, related_status=self, ) - def delete(self, *args, **kwargs):#pylint: disable=unused-argument - ''' "delete" a status ''' - if hasattr(self, 'boosted_status'): + def delete(self, *args, **kwargs): # pylint: disable=unused-argument + """ "delete" a status """ + if hasattr(self, "boosted_status"): # okay but if it's a boost really delete it super().delete(*args, **kwargs) return @@ -96,141 +102,159 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @property def recipients(self): - ''' tagged users who definitely need to get this status in broadcast ''' + """ tagged users who definitely need to get this status in broadcast """ mentions = [u for u in self.mention_users.all() if not u.local] - if hasattr(self, 'reply_parent') and self.reply_parent \ - and not self.reply_parent.user.local: + if ( + hasattr(self, "reply_parent") + and self.reply_parent + and not self.reply_parent.user.local + ): mentions.append(self.reply_parent.user) return list(set(mentions)) @classmethod def ignore_activity(cls, activity): - ''' keep notes if they are replies to existing statuses ''' - if activity.type == 'Announce': - # keep it if the booster or the boosted are local - boosted = activitypub.resolve_remote_id(activity.object, save=False) + """ keep notes if they are replies to existing statuses """ + if activity.type == "Announce": + try: + boosted = activitypub.resolve_remote_id(activity.object, save=False) + except activitypub.ActivitySerializerError: + # if we can't load the status, definitely ignore it + return True + # keep the boost if we would keep the status return cls.ignore_activity(boosted.to_activity_dataclass()) # keep if it if it's a custom type - if activity.type != 'Note': + if activity.type != "Note": return False - if cls.objects.filter( - remote_id=activity.inReplyTo).exists(): + # keep it if it's a reply to an existing status + if cls.objects.filter(remote_id=activity.inReplyTo).exists(): return False # keep notes if they mention local users if activity.tag == MISSING or activity.tag is None: return True - tags = [l['href'] for l in activity.tag if l['type'] == 'Mention'] - user_model = apps.get_model('bookwyrm.User', require_ready=True) + tags = [l["href"] for l in activity.tag if l["type"] == "Mention"] + user_model = apps.get_model("bookwyrm.User", require_ready=True) for tag in tags: - if user_model.objects.filter( - remote_id=tag, local=True).exists(): + if user_model.objects.filter(remote_id=tag, local=True).exists(): # we found a mention of a known use boost return False return True @classmethod def replies(cls, status): - ''' load all replies to a status. idk if there's a better way - to write this so it's just a property ''' - return cls.objects.filter( - reply_parent=status - ).select_subclasses().order_by('published_date') + """load all replies to a status. idk if there's a better way + to write this so it's just a property""" + return ( + cls.objects.filter(reply_parent=status) + .select_subclasses() + .order_by("published_date") + ) @property def status_type(self): - ''' expose the type of status for the ui using activity type ''' + """ expose the type of status for the ui using activity type """ return self.activity_serializer.__name__ @property def boostable(self): - ''' you can't boost dms ''' - return self.privacy in ['unlisted', 'public'] + """ you can't boost dms """ + return self.privacy in ["unlisted", "public"] def to_replies(self, **kwargs): - ''' helper function for loading AP serialized replies to a status ''' + """ helper function for loading AP serialized replies to a status """ return self.to_ordered_collection( self.replies(self), - remote_id='%s/replies' % self.remote_id, + remote_id="%s/replies" % self.remote_id, collection_only=True, **kwargs ).serialize() - def to_activity_dataclass(self, pure=False):# pylint: disable=arguments-differ - ''' return tombstone if the status is deleted ''' + def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ + """ return tombstone if the status is deleted """ if self.deleted: return activitypub.Tombstone( id=self.remote_id, url=self.remote_id, deleted=self.deleted_date.isoformat(), - published=self.deleted_date.isoformat() + published=self.deleted_date.isoformat(), ) activity = ActivitypubMixin.to_activity_dataclass(self) activity.replies = self.to_replies() # "pure" serialization for non-bookwyrm instances - if pure and hasattr(self, 'pure_content'): + if pure and hasattr(self, "pure_content"): activity.content = self.pure_content - if hasattr(activity, 'name'): + if hasattr(activity, "name"): activity.name = self.pure_name activity.type = self.pure_type activity.attachment = [ - image_serializer(b.cover, b.alt_text) \ - for b in self.mention_books.all()[:4] if b.cover] - if hasattr(self, 'book') and self.book.cover: + image_serializer(b.cover, b.alt_text) + for b in self.mention_books.all()[:4] + if b.cover + ] + if hasattr(self, "book") and self.book.cover: activity.attachment.append( image_serializer(self.book.cover, self.book.alt_text) ) return activity - def to_activity(self, pure=False):# pylint: disable=arguments-differ - ''' json serialized activitypub class ''' + def to_activity(self, pure=False): # pylint: disable=arguments-differ + """ json serialized activitypub class """ return self.to_activity_dataclass(pure=pure).serialize() class GeneratedNote(Status): - ''' these are app-generated messages about user activity ''' + """ these are app-generated messages about user activity """ + @property def pure_content(self): - ''' indicate the book in question for mastodon (or w/e) users ''' + """ indicate the book in question for mastodon (or w/e) users """ message = self.content - books = ', '.join( - '"%s"' % (book.remote_id, book.title) \ + books = ", ".join( + '"%s"' % (book.remote_id, book.title) for book in self.mention_books.all() ) - return '%s %s %s' % (self.user.display_name, message, books) + return "%s %s %s" % (self.user.display_name, message, books) activity_serializer = activitypub.GeneratedNote - pure_type = 'Note' + pure_type = "Note" class Comment(Status): - ''' like a review but without a rating and transient ''' + """ like a review but without a rating and transient """ + book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook') + "Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook" + ) @property def pure_content(self): - ''' indicate the book in question for mastodon (or w/e) users ''' - return '%s

(comment on "%s")

' % \ - (self.content, self.book.remote_id, self.book.title) + """ indicate the book in question for mastodon (or w/e) users """ + return '%s

(comment on "%s")

' % ( + self.content, + self.book.remote_id, + self.book.title, + ) activity_serializer = activitypub.Comment - pure_type = 'Note' + pure_type = "Note" class Quotation(Status): - ''' like a review but without a rating and transient ''' + """ like a review but without a rating and transient """ + quote = fields.HtmlField() book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook') + "Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook" + ) @property def pure_content(self): - ''' indicate the book in question for mastodon (or w/e) users ''' - quote = re.sub(r'^

', '

"', self.quote) - quote = re.sub(r'

$', '"

', quote) + """ indicate the book in question for mastodon (or w/e) users """ + quote = re.sub(r"^

", '

"', self.quote) + quote = re.sub(r"

$", '"

', quote) return '%s

-- "%s"

%s' % ( quote, self.book.remote_id, @@ -239,90 +263,104 @@ class Quotation(Status): ) activity_serializer = activitypub.Quotation - pure_type = 'Note' + pure_type = "Note" class Review(Status): - ''' a book review ''' + """ a book review """ + name = fields.CharField(max_length=255, null=True) book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook') - rating = fields.IntegerField( + "Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook" + ) + rating = fields.DecimalField( default=None, null=True, blank=True, - validators=[MinValueValidator(1), MaxValueValidator(5)] + validators=[MinValueValidator(1), MaxValueValidator(5)], + decimal_places=2, + max_digits=3, ) @property def pure_name(self): - ''' clarify review names for mastodon serialization ''' + """ clarify review names for mastodon serialization """ if self.rating: - #pylint: disable=bad-string-format-type - return 'Review of "%s" (%d stars): %s' % ( + return 'Review of "{}" ({:d} stars): {}'.format( self.book.title, self.rating, - self.name + self.name, ) - return 'Review of "%s": %s' % ( - self.book.title, - self.name - ) + return 'Review of "{}": {}'.format(self.book.title, self.name) @property def pure_content(self): - ''' indicate the book in question for mastodon (or w/e) users ''' + """ indicate the book in question for mastodon (or w/e) users """ return self.content activity_serializer = activitypub.Review - pure_type = 'Article' + pure_type = "Article" + + +class ReviewRating(Review): + """ a subtype of review that only contains a rating """ + + def save(self, *args, **kwargs): + if not self.rating: + raise ValueError("ReviewRating object must include a numerical rating") + return super().save(*args, **kwargs) + + @property + def pure_content(self): + template = get_template("snippets/generated_status/rating.html") + return template.render({"book": self.book, "rating": self.rating}).strip() + + activity_serializer = activitypub.Rating + pure_type = "Note" class Boost(ActivityMixin, Status): - ''' boost'ing a post ''' + """ boost'ing a post """ + boosted_status = fields.ForeignKey( - 'Status', + "Status", on_delete=models.PROTECT, - related_name='boosters', - activitypub_field='object', + related_name="boosters", + activitypub_field="object", ) activity_serializer = activitypub.Announce def save(self, *args, **kwargs): - ''' save and notify ''' + """ save and notify """ super().save(*args, **kwargs) if not self.boosted_status.user.local: return - notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) notification_model.objects.create( user=self.boosted_status.user, related_status=self.boosted_status, related_user=self.user, - notification_type='BOOST', + notification_type="BOOST", ) def delete(self, *args, **kwargs): - ''' delete and un-notify ''' - notification_model = apps.get_model( - 'bookwyrm.Notification', require_ready=True) + """ delete and un-notify """ + notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) notification_model.objects.filter( user=self.boosted_status.user, related_status=self.boosted_status, related_user=self.user, - notification_type='BOOST', + notification_type="BOOST", ).delete() super().delete(*args, **kwargs) - def __init__(self, *args, **kwargs): - ''' the user field is "actor" here instead of "attributedTo" ''' + """ the user field is "actor" here instead of "attributedTo" """ super().__init__(*args, **kwargs) - reserve_fields = ['user', 'boosted_status'] - self.simple_fields = [f for f in self.simple_fields if \ - f.name in reserve_fields] + reserve_fields = ["user", "boosted_status"] + self.simple_fields = [f for f in self.simple_fields if f.name in reserve_fields] self.activity_fields = self.simple_fields self.many_to_many_fields = [] self.image_fields = [] diff --git a/bookwyrm/models/tag.py b/bookwyrm/models/tag.py index 83359170a..2c45b8f91 100644 --- a/bookwyrm/models/tag.py +++ b/bookwyrm/models/tag.py @@ -1,4 +1,4 @@ -''' models for storing different kinds of Activities ''' +""" models for storing different kinds of Activities """ import urllib.parse from django.apps import apps @@ -12,28 +12,30 @@ from . import fields class Tag(OrderedCollectionMixin, BookWyrmModel): - ''' freeform tags for books ''' + """ freeform tags for books """ + name = fields.CharField(max_length=100, unique=True) identifier = models.CharField(max_length=100) @property def books(self): - ''' count of books associated with this tag ''' - edition_model = apps.get_model('bookwyrm.Edition', require_ready=True) - return edition_model.objects.filter( - usertag__tag__identifier=self.identifier - ).order_by('-created_date').distinct() + """ count of books associated with this tag """ + edition_model = apps.get_model("bookwyrm.Edition", require_ready=True) + return ( + edition_model.objects.filter(usertag__tag__identifier=self.identifier) + .order_by("-created_date") + .distinct() + ) collection_queryset = books def get_remote_id(self): - ''' tag should use identifier not id in remote_id ''' - base_path = 'https://%s' % DOMAIN - return '%s/tag/%s' % (base_path, self.identifier) - + """ tag should use identifier not id in remote_id """ + base_path = "https://%s" % DOMAIN + return "%s/tag/%s" % (base_path, self.identifier) def save(self, *args, **kwargs): - ''' create a url-safe lookup key for the tag ''' + """ create a url-safe lookup key for the tag """ if not self.id: # add identifiers to new tags self.identifier = urllib.parse.quote_plus(self.name) @@ -41,18 +43,21 @@ class Tag(OrderedCollectionMixin, BookWyrmModel): class UserTag(CollectionItemMixin, BookWyrmModel): - ''' an instance of a tag on a book by a user ''' + """ an instance of a tag on a book by a user """ + user = fields.ForeignKey( - 'User', on_delete=models.PROTECT, activitypub_field='actor') + "User", on_delete=models.PROTECT, activitypub_field="actor" + ) book = fields.ForeignKey( - 'Edition', on_delete=models.PROTECT, activitypub_field='object') - tag = fields.ForeignKey( - 'Tag', on_delete=models.PROTECT, activitypub_field='target') + "Edition", on_delete=models.PROTECT, activitypub_field="object" + ) + tag = fields.ForeignKey("Tag", on_delete=models.PROTECT, activitypub_field="target") activity_serializer = activitypub.Add - object_field = 'book' - collection_field = 'tag' + object_field = "book" + collection_field = "tag" class Meta: - ''' unqiueness constraint ''' - unique_together = ('user', 'book', 'tag') + """ unqiueness constraint """ + + unique_together = ("user", "book", "tag") diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index bbeb10ccb..46f08509f 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -1,4 +1,4 @@ -''' database schema for user data ''' +""" database schema for user data """ import re from urllib.parse import urlparse @@ -23,25 +23,28 @@ from . import fields, Review class User(OrderedCollectionPageMixin, AbstractUser): - ''' a user who wants to read books ''' + """ a user who wants to read books """ + username = fields.UsernameField() email = models.EmailField(unique=True, null=True) key_pair = fields.OneToOneField( - 'KeyPair', + "KeyPair", on_delete=models.CASCADE, - blank=True, null=True, - activitypub_field='publicKey', - related_name='owner' + blank=True, + null=True, + activitypub_field="publicKey", + related_name="owner", ) inbox = fields.RemoteIdField(unique=True) shared_inbox = fields.RemoteIdField( - activitypub_field='sharedInbox', - activitypub_wrapper='endpoints', + activitypub_field="sharedInbox", + activitypub_wrapper="endpoints", deduplication_field=False, - null=True) + null=True, + ) federated_server = models.ForeignKey( - 'FederatedServer', + "FederatedServer", on_delete=models.PROTECT, null=True, blank=True, @@ -59,54 +62,59 @@ class User(OrderedCollectionPageMixin, AbstractUser): # name is your display name, which you can change at will name = fields.CharField(max_length=100, null=True, blank=True) avatar = fields.ImageField( - upload_to='avatars/', blank=True, null=True, - activitypub_field='icon', alt_field='alt_text') + upload_to="avatars/", + blank=True, + null=True, + activitypub_field="icon", + alt_field="alt_text", + ) followers = fields.ManyToManyField( - 'self', + "self", link_only=True, symmetrical=False, - through='UserFollows', - through_fields=('user_object', 'user_subject'), - related_name='following' + through="UserFollows", + through_fields=("user_object", "user_subject"), + related_name="following", ) follow_requests = models.ManyToManyField( - 'self', + "self", symmetrical=False, - through='UserFollowRequest', - through_fields=('user_subject', 'user_object'), - related_name='follower_requests' + through="UserFollowRequest", + through_fields=("user_subject", "user_object"), + related_name="follower_requests", ) blocks = models.ManyToManyField( - 'self', + "self", symmetrical=False, - through='UserBlocks', - through_fields=('user_subject', 'user_object'), - related_name='blocked_by' + through="UserBlocks", + through_fields=("user_subject", "user_object"), + related_name="blocked_by", ) favorites = models.ManyToManyField( - 'Status', + "Status", symmetrical=False, - through='Favorite', - through_fields=('user', 'status'), - related_name='favorite_statuses' + through="Favorite", + through_fields=("user", "status"), + related_name="favorite_statuses", ) - remote_id = fields.RemoteIdField( - null=True, unique=True, activitypub_field='id') + remote_id = fields.RemoteIdField(null=True, unique=True, activitypub_field="id") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) last_active_date = models.DateTimeField(auto_now=True) manually_approves_followers = fields.BooleanField(default=False) + show_goal = models.BooleanField(default=True) + + name_field = "username" - name_field = 'username' @property def alt_text(self): - ''' alt text with username ''' - return 'avatar for %s' % (self.localname or self.username) + """ alt text with username """ + return "avatar for %s" % (self.localname or self.username) @property def display_name(self): - ''' show the cleanest version of the user's name possible ''' - if self.name and self.name != '': + """ show the cleanest version of the user's name possible """ + if self.name and self.name != "": return self.name return self.localname or self.username @@ -114,78 +122,82 @@ class User(OrderedCollectionPageMixin, AbstractUser): @classmethod def viewer_aware_objects(cls, viewer): - ''' the user queryset filtered for the context of the logged in user ''' + """ the user queryset filtered for the context of the logged in user """ queryset = cls.objects.filter(is_active=True) if viewer.is_authenticated: - queryset = queryset.exclude( - blocks=viewer - ) + queryset = queryset.exclude(blocks=viewer) return queryset def to_outbox(self, filter_type=None, **kwargs): - ''' an ordered collection of statuses ''' + """ an ordered collection of statuses """ if filter_type: filter_class = apps.get_model( - 'bookwyrm.%s' % filter_type, require_ready=True) + "bookwyrm.%s" % filter_type, require_ready=True + ) if not issubclass(filter_class, Status): raise TypeError( - 'filter_status_class must be a subclass of models.Status') + "filter_status_class must be a subclass of models.Status" + ) queryset = filter_class.objects else: queryset = Status.objects - queryset = queryset.filter( - user=self, - deleted=False, - privacy__in=['public', 'unlisted'], - ).select_subclasses().order_by('-published_date') - return self.to_ordered_collection(queryset, \ - collection_only=True, remote_id=self.outbox, **kwargs).serialize() + queryset = ( + queryset.filter( + user=self, + deleted=False, + privacy__in=["public", "unlisted"], + ) + .select_subclasses() + .order_by("-published_date") + ) + return self.to_ordered_collection( + queryset, collection_only=True, remote_id=self.outbox, **kwargs + ).serialize() def to_following_activity(self, **kwargs): - ''' activitypub following list ''' - remote_id = '%s/following' % self.remote_id + """ activitypub following list """ + remote_id = "%s/following" % self.remote_id return self.to_ordered_collection( - self.following.order_by('-updated_date').all(), + self.following.order_by("-updated_date").all(), remote_id=remote_id, id_only=True, **kwargs ) def to_followers_activity(self, **kwargs): - ''' activitypub followers list ''' - remote_id = '%s/followers' % self.remote_id + """ activitypub followers list """ + remote_id = "%s/followers" % self.remote_id return self.to_ordered_collection( - self.followers.order_by('-updated_date').all(), + self.followers.order_by("-updated_date").all(), remote_id=remote_id, id_only=True, **kwargs ) def to_activity(self): - ''' override default AP serializer to add context object - idk if this is the best way to go about this ''' + """override default AP serializer to add context object + idk if this is the best way to go about this""" activity_object = super().to_activity() - activity_object['@context'] = [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + activity_object["@context"] = [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", { - 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers', - 'schema': 'http://schema.org#', - 'PropertyValue': 'schema:PropertyValue', - 'value': 'schema:value', - } + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + }, ] return activity_object - def save(self, *args, **kwargs): - ''' populate fields for new local users ''' + """ populate fields for new local users """ created = not bool(self.id) if not self.local and not re.match(regex.full_username, self.username): # generate a username that uses the domain (webfinger format) actor_parts = urlparse(self.remote_id) - self.username = '%s@%s' % (self.username, actor_parts.netloc) + self.username = "%s@%s" % (self.username, actor_parts.netloc) super().save(*args, **kwargs) # this user already exists, no need to populate fields @@ -200,114 +212,120 @@ class User(OrderedCollectionPageMixin, AbstractUser): return # populate fields for local users - self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.localname) - self.inbox = '%s/inbox' % self.remote_id - self.shared_inbox = 'https://%s/inbox' % DOMAIN - self.outbox = '%s/outbox' % self.remote_id + self.remote_id = "https://%s/user/%s" % (DOMAIN, self.localname) + self.inbox = "%s/inbox" % self.remote_id + self.shared_inbox = "https://%s/inbox" % DOMAIN + self.outbox = "%s/outbox" % self.remote_id # an id needs to be set before we can proceed with related models super().save(*args, **kwargs) # make users editors by default try: - self.groups.add(Group.objects.get(name='editor')) + self.groups.add(Group.objects.get(name="editor")) except Group.DoesNotExist: # this should only happen in tests pass # create keys and shelves for new local users self.key_pair = KeyPair.objects.create( - remote_id='%s/#main-key' % self.remote_id) + remote_id="%s/#main-key" % self.remote_id + ) self.save(broadcast=False) - shelves = [{ - 'name': 'To Read', - 'identifier': 'to-read', - }, { - 'name': 'Currently Reading', - 'identifier': 'reading', - }, { - 'name': 'Read', - 'identifier': 'read', - }] + shelves = [ + { + "name": "To Read", + "identifier": "to-read", + }, + { + "name": "Currently Reading", + "identifier": "reading", + }, + { + "name": "Read", + "identifier": "read", + }, + ] for shelf in shelves: Shelf( - name=shelf['name'], - identifier=shelf['identifier'], + name=shelf["name"], + identifier=shelf["identifier"], user=self, - editable=False + editable=False, ).save(broadcast=False) @property def local_path(self): - ''' this model doesn't inherit bookwyrm model, so here we are ''' - return '/user/%s' % (self.localname or self.username) + """ this model doesn't inherit bookwyrm model, so here we are """ + return "/user/%s" % (self.localname or self.username) class KeyPair(ActivitypubMixin, BookWyrmModel): - ''' public and private keys for a user ''' + """ public and private keys for a user """ + private_key = models.TextField(blank=True, null=True) public_key = fields.TextField( - blank=True, null=True, activitypub_field='publicKeyPem') + blank=True, null=True, activitypub_field="publicKeyPem" + ) activity_serializer = activitypub.PublicKey - serialize_reverse_fields = [('owner', 'owner', 'id')] + serialize_reverse_fields = [("owner", "owner", "id")] def get_remote_id(self): # self.owner is set by the OneToOneField on User - return '%s/#main-key' % self.owner.remote_id + return "%s/#main-key" % self.owner.remote_id def save(self, *args, **kwargs): - ''' create a key pair ''' + """ create a key pair """ # no broadcasting happening here - if 'broadcast' in kwargs: - del kwargs['broadcast'] + if "broadcast" in kwargs: + del kwargs["broadcast"] if not self.public_key: self.private_key, self.public_key = create_key_pair() return super().save(*args, **kwargs) def to_activity(self): - ''' override default AP serializer to add context object - idk if this is the best way to go about this ''' + """override default AP serializer to add context object + idk if this is the best way to go about this""" activity_object = super().to_activity() - del activity_object['@context'] - del activity_object['type'] + del activity_object["@context"] + del activity_object["type"] return activity_object class AnnualGoal(BookWyrmModel): - ''' set a goal for how many books you read in a year ''' - user = models.ForeignKey('User', on_delete=models.PROTECT) - goal = models.IntegerField( - validators=[MinValueValidator(1)] - ) + """ set a goal for how many books you read in a year """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + goal = models.IntegerField(validators=[MinValueValidator(1)]) year = models.IntegerField(default=timezone.now().year) privacy = models.CharField( - max_length=255, - default='public', - choices=fields.PrivacyLevels.choices + max_length=255, default="public", choices=fields.PrivacyLevels.choices ) class Meta: - ''' unqiueness constraint ''' - unique_together = ('user', 'year') + """ unqiueness constraint """ + + unique_together = ("user", "year") def get_remote_id(self): - ''' put the year in the path ''' - return '%s/goal/%d' % (self.user.remote_id, self.year) + """ put the year in the path """ + return "%s/goal/%d" % (self.user.remote_id, self.year) @property def books(self): - ''' the books you've read this year ''' - return self.user.readthrough_set.filter( - finish_date__year__gte=self.year - ).order_by('-finish_date').all() - + """ the books you've read this year """ + return ( + self.user.readthrough_set.filter(finish_date__year__gte=self.year) + .order_by("-finish_date") + .all() + ) @property def ratings(self): - ''' ratings for books read this year ''' + """ ratings for books read this year """ book_ids = [r.book.id for r in self.books] reviews = Review.objects.filter( user=self.user, @@ -315,55 +333,50 @@ class AnnualGoal(BookWyrmModel): ) return {r.book.id: r.rating for r in reviews} - @property def progress_percent(self): - ''' how close to your goal, in percent form ''' + """ how close to your goal, in percent form """ return int(float(self.book_count / self.goal) * 100) - @property def book_count(self): - ''' how many books you've read this year ''' + """ how many books you've read this year """ return self.user.readthrough_set.filter( - finish_date__year__gte=self.year).count() + finish_date__year__gte=self.year + ).count() @app.task def set_remote_server(user_id): - ''' figure out the user's remote server in the background ''' + """ figure out the user's remote server in the background """ user = User.objects.get(id=user_id) actor_parts = urlparse(user.remote_id) - user.federated_server = \ - get_or_create_remote_server(actor_parts.netloc) + user.federated_server = get_or_create_remote_server(actor_parts.netloc) user.save(broadcast=False) if user.bookwyrm_user: get_remote_reviews.delay(user.outbox) def get_or_create_remote_server(domain): - ''' get info on a remote server ''' + """ get info on a remote server """ try: - return FederatedServer.objects.get( - server_name=domain - ) + return FederatedServer.objects.get(server_name=domain) except FederatedServer.DoesNotExist: pass try: - data = get_data('https://%s/.well-known/nodeinfo' % domain) + data = get_data("https://%s/.well-known/nodeinfo" % domain) try: - nodeinfo_url = data.get('links')[0].get('href') + nodeinfo_url = data.get("links")[0].get("href") except (TypeError, KeyError): raise ConnectorException() data = get_data(nodeinfo_url) - application_type = data.get('software', {}).get('name') - application_version = data.get('software', {}).get('version') + application_type = data.get("software", {}).get("name") + application_version = data.get("software", {}).get("version") except ConnectorException: application_type = application_version = None - server = FederatedServer.objects.create( server_name=domain, application_type=application_type, @@ -374,12 +387,12 @@ def get_or_create_remote_server(domain): @app.task def get_remote_reviews(outbox): - ''' ingest reviews by a new remote bookwyrm user ''' - outbox_page = outbox + '?page=true&type=Review' + """ ingest reviews by a new remote bookwyrm user """ + outbox_page = outbox + "?page=true&type=Review" data = get_data(outbox_page) # TODO: pagination? - for activity in data['orderedItems']: - if not activity['type'] == 'Review': + for activity in data["orderedItems"]: + if not activity["type"] == "Review": continue activitypub.Review(**activity).to_model() diff --git a/bookwyrm/sanitize_html.py b/bookwyrm/sanitize_html.py index be7fb56fd..2a630f838 100644 --- a/bookwyrm/sanitize_html.py +++ b/bookwyrm/sanitize_html.py @@ -1,56 +1,63 @@ -''' html parser to clean up incoming text from unknown sources ''' +""" html parser to clean up incoming text from unknown sources """ from html.parser import HTMLParser -class InputHtmlParser(HTMLParser):#pylint: disable=abstract-method - ''' Removes any html that isn't allowed_tagsed from a block ''' + +class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method + """ Removes any html that isn't allowed_tagsed from a block """ def __init__(self): HTMLParser.__init__(self) self.allowed_tags = [ - 'p', 'blockquote', 'br', - 'b', 'i', 'strong', 'em', 'pre', - 'a', 'span', 'ul', 'ol', 'li' + "p", + "blockquote", + "br", + "b", + "i", + "strong", + "em", + "pre", + "a", + "span", + "ul", + "ol", + "li", ] self.tag_stack = [] self.output = [] # if the html appears invalid, we just won't allow any at all self.allow_html = True - def handle_starttag(self, tag, attrs): - ''' check if the tag is valid ''' + """ check if the tag is valid """ if self.allow_html and tag in self.allowed_tags: - self.output.append(('tag', self.get_starttag_text())) + self.output.append(("tag", self.get_starttag_text())) self.tag_stack.append(tag) else: - self.output.append(('data', '')) - + self.output.append(("data", "")) def handle_endtag(self, tag): - ''' keep the close tag ''' + """ keep the close tag """ if not self.allow_html or tag not in self.allowed_tags: - self.output.append(('data', '')) + self.output.append(("data", "")) return if not self.tag_stack or self.tag_stack[-1] != tag: # the end tag doesn't match the most recent start tag self.allow_html = False - self.output.append(('data', '')) + self.output.append(("data", "")) return self.tag_stack = self.tag_stack[:-1] - self.output.append(('tag', '' % tag)) - + self.output.append(("tag", "" % tag)) def handle_data(self, data): - ''' extract the answer, if we're in an answer tag ''' - self.output.append(('data', data)) - + """ extract the answer, if we're in an answer tag """ + self.output.append(("data", data)) def get_output(self): - ''' convert the output from a list of tuples to a string ''' + """ convert the output from a list of tuples to a string """ if self.tag_stack: self.allow_html = False if not self.allow_html: - return ''.join(v for (k, v) in self.output if k == 'data') - return ''.join(v for (k, v) in self.output) + return "".join(v for (k, v) in self.output if k == "data") + return "".join(v for (k, v) in self.output) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 45fdbd9da..bcff58287 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -1,4 +1,4 @@ -''' bookwyrm settings and configuration ''' +""" bookwyrm settings and configuration """ import os from environs import Env @@ -7,129 +7,129 @@ from django.utils.translation import gettext_lazy as _ env = Env() -DOMAIN = env('DOMAIN') -VERSION = '0.0.1' +DOMAIN = env("DOMAIN") +VERSION = "0.0.1" -PAGE_LENGTH = env('PAGE_LENGTH', 15) +PAGE_LENGTH = env("PAGE_LENGTH", 15) # celery -CELERY_BROKER = env('CELERY_BROKER') -CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND') -CELERY_ACCEPT_CONTENT = ['application/json'] -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' +CELERY_BROKER = env("CELERY_BROKER") +CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND") +CELERY_ACCEPT_CONTENT = ["application/json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" # email -EMAIL_HOST = env('EMAIL_HOST') -EMAIL_PORT = env('EMAIL_PORT', 587) -EMAIL_HOST_USER = env('EMAIL_HOST_USER') -EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') -EMAIL_USE_TLS = env('EMAIL_USE_TLS', True) +EMAIL_HOST = env("EMAIL_HOST") +EMAIL_PORT = env("EMAIL_PORT", 587) +EMAIL_HOST_USER = env("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") +EMAIL_USE_TLS = env("EMAIL_USE_TLS", True) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'),] +LOCALE_PATHS = [ + os.path.join(BASE_DIR, "locale"), +] # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = env('SECRET_KEY') +SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool('DEBUG', True) +DEBUG = env.bool("DEBUG", True) -ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', ['*']) -OL_URL = env('OL_URL') +ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"]) +OL_URL = env("OL_URL") # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - 'django_rename_app', - 'bookwyrm', - 'celery', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", + "django_rename_app", + "bookwyrm", + "celery", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'bookwyrm.urls' +ROOT_URLCONF = "bookwyrm.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': ['templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'bookwyrm.context_processors.site_settings', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "bookwyrm.context_processors.site_settings", ], }, }, ] -WSGI_APPLICATION = 'bookwyrm.wsgi.application' +WSGI_APPLICATION = "bookwyrm.wsgi.application" # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases -BOOKWYRM_DATABASE_BACKEND = env('BOOKWYRM_DATABASE_BACKEND', 'postgres') +BOOKWYRM_DATABASE_BACKEND = env("BOOKWYRM_DATABASE_BACKEND", "postgres") BOOKWYRM_DBS = { - 'postgres': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': env('POSTGRES_DB', 'fedireads'), - 'USER': env('POSTGRES_USER', 'fedireads'), - 'PASSWORD': env('POSTGRES_PASSWORD', 'fedireads'), - 'HOST': env('POSTGRES_HOST', ''), - 'PORT': 5432 + "postgres": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": env("POSTGRES_DB", "fedireads"), + "USER": env("POSTGRES_USER", "fedireads"), + "PASSWORD": env("POSTGRES_PASSWORD", "fedireads"), + "HOST": env("POSTGRES_HOST", ""), + "PORT": 5432, }, } -DATABASES = { - 'default': BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND] -} +DATABASES = {"default": BOOKWYRM_DBS[BOOKWYRM_DATABASE_BACKEND]} -LOGIN_URL = '/login/' -AUTH_USER_MODEL = 'bookwyrm.User' +LOGIN_URL = "/login/" +AUTH_USER_MODEL = "bookwyrm.User" # Password validation # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -137,17 +137,17 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" LANGUAGES = [ - ('en-us', _('English')), - ('de-de', _('German')), - ('es', _('Spanish')), - ('fr-fr', _('French')), - ('zh-cn', _('Simplified Chinese')), + ("en-us", _("English")), + ("de-de", _("German")), + ("es", _("Spanish")), + ("fr-fr", _("French")), + ("zh-cn", _("Simplified Chinese")), ] -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -160,10 +160,13 @@ USE_TZ = True # https://docs.djangoproject.com/en/2.0/howto/static-files/ PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, env('STATIC_ROOT', 'static')) -MEDIA_URL = '/images/' -MEDIA_ROOT = os.path.join(BASE_DIR, env('MEDIA_ROOT', 'images')) +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) +MEDIA_URL = "/images/" +MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % ( - requests.utils.default_user_agent(), VERSION, DOMAIN) + requests.utils.default_user_agent(), + VERSION, + DOMAIN, +) diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py index ff2816640..80cbfdc79 100644 --- a/bookwyrm/signatures.py +++ b/bookwyrm/signatures.py @@ -1,4 +1,4 @@ -''' signs activitypub activities ''' +""" signs activitypub activities """ import hashlib from urllib.parse import urlparse import datetime @@ -6,54 +6,56 @@ from base64 import b64encode, b64decode from Crypto import Random from Crypto.PublicKey import RSA -from Crypto.Signature import pkcs1_15 #pylint: disable=no-name-in-module +from Crypto.Signature import pkcs1_15 # pylint: disable=no-name-in-module from Crypto.Hash import SHA256 MAX_SIGNATURE_AGE = 300 + def create_key_pair(): - ''' a new public/private key pair, used for creating new users ''' + """ a new public/private key pair, used for creating new users """ random_generator = Random.new().read key = RSA.generate(1024, random_generator) - private_key = key.export_key().decode('utf8') - public_key = key.publickey().export_key().decode('utf8') + private_key = key.export_key().decode("utf8") + public_key = key.publickey().export_key().decode("utf8") return private_key, public_key def make_signature(sender, destination, date, digest): - ''' uses a private key to sign an outgoing message ''' + """ uses a private key to sign an outgoing message """ inbox_parts = urlparse(destination) signature_headers = [ - '(request-target): post %s' % inbox_parts.path, - 'host: %s' % inbox_parts.netloc, - 'date: %s' % date, - 'digest: %s' % digest, + "(request-target): post %s" % inbox_parts.path, + "host: %s" % inbox_parts.netloc, + "date: %s" % date, + "digest: %s" % digest, ] - message_to_sign = '\n'.join(signature_headers) + message_to_sign = "\n".join(signature_headers) signer = pkcs1_15.new(RSA.import_key(sender.key_pair.private_key)) - signed_message = signer.sign(SHA256.new(message_to_sign.encode('utf8'))) + signed_message = signer.sign(SHA256.new(message_to_sign.encode("utf8"))) signature = { - 'keyId': '%s#main-key' % sender.remote_id, - 'algorithm': 'rsa-sha256', - 'headers': '(request-target) host date digest', - 'signature': b64encode(signed_message).decode('utf8'), + "keyId": "%s#main-key" % sender.remote_id, + "algorithm": "rsa-sha256", + "headers": "(request-target) host date digest", + "signature": b64encode(signed_message).decode("utf8"), } - return ','.join('%s="%s"' % (k, v) for (k, v) in signature.items()) + return ",".join('%s="%s"' % (k, v) for (k, v) in signature.items()) def make_digest(data): - ''' creates a message digest for signing ''' - return 'SHA-256=' + b64encode(hashlib.sha256(data.encode('utf-8'))\ - .digest()).decode('utf-8') + """ creates a message digest for signing """ + return "SHA-256=" + b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode( + "utf-8" + ) def verify_digest(request): - ''' checks if a digest is syntactically valid and matches the message ''' - algorithm, digest = request.headers['digest'].split('=', 1) - if algorithm == 'SHA-256': + """ checks if a digest is syntactically valid and matches the message """ + algorithm, digest = request.headers["digest"].split("=", 1) + if algorithm == "SHA-256": hash_function = hashlib.sha256 - elif algorithm == 'SHA-512': + elif algorithm == "SHA-512": hash_function = hashlib.sha512 else: raise ValueError("Unsupported hash function: {}".format(algorithm)) @@ -62,8 +64,10 @@ def verify_digest(request): if b64decode(digest) != expected: raise ValueError("Invalid HTTP Digest header") + class Signature: - ''' read and validate incoming signatures ''' + """ read and validate incoming signatures """ + def __init__(self, key_id, headers, signature): self.key_id = key_id self.headers = headers @@ -71,42 +75,39 @@ class Signature: @classmethod def parse(cls, request): - ''' extract and parse a signature from an http request ''' + """ extract and parse a signature from an http request """ signature_dict = {} - for pair in request.headers['Signature'].split(','): - k, v = pair.split('=', 1) - v = v.replace('"', '') + for pair in request.headers["Signature"].split(","): + k, v = pair.split("=", 1) + v = v.replace('"', "") signature_dict[k] = v try: - key_id = signature_dict['keyId'] - headers = signature_dict['headers'] - signature = b64decode(signature_dict['signature']) + key_id = signature_dict["keyId"] + headers = signature_dict["headers"] + signature = b64decode(signature_dict["signature"]) except KeyError: - raise ValueError('Invalid auth header') + raise ValueError("Invalid auth header") return cls(key_id, headers, signature) def verify(self, public_key, request): - ''' verify rsa signature ''' - if http_date_age(request.headers['date']) > MAX_SIGNATURE_AGE: - raise ValueError( - "Request too old: %s" % (request.headers['date'],)) + """ verify rsa signature """ + if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE: + raise ValueError("Request too old: %s" % (request.headers["date"],)) public_key = RSA.import_key(public_key) comparison_string = [] - for signed_header_name in self.headers.split(' '): - if signed_header_name == '(request-target)': - comparison_string.append( - '(request-target): post %s' % request.path) + for signed_header_name in self.headers.split(" "): + if signed_header_name == "(request-target)": + comparison_string.append("(request-target): post %s" % request.path) else: - if signed_header_name == 'digest': + if signed_header_name == "digest": verify_digest(request) - comparison_string.append('%s: %s' % ( - signed_header_name, - request.headers[signed_header_name] - )) - comparison_string = '\n'.join(comparison_string) + comparison_string.append( + "%s: %s" % (signed_header_name, request.headers[signed_header_name]) + ) + comparison_string = "\n".join(comparison_string) signer = pkcs1_15.new(public_key) digest = SHA256.new() @@ -117,7 +118,7 @@ class Signature: def http_date_age(datestr): - ''' age of a signature in seconds ''' - parsed = datetime.datetime.strptime(datestr, '%a, %d %b %Y %H:%M:%S GMT') + """ age of a signature in seconds """ + parsed = datetime.datetime.strptime(datestr, "%a, %d %b %Y %H:%M:%S GMT") delta = datetime.datetime.utcnow() - parsed return delta.total_seconds() diff --git a/bookwyrm/static/js/check_all.js b/bookwyrm/static/js/check_all.js new file mode 100644 index 000000000..ea2300ced --- /dev/null +++ b/bookwyrm/static/js/check_all.js @@ -0,0 +1,17 @@ +// Toggle all checkboxes. + +/** + * Toggle all descendant checkboxes of a target. + * + * Use `data-target="ID_OF_TARGET"` on the node being listened to. + * + * @param {Event} event - change Event + * @return {undefined} + */ +function toggleAllCheckboxes(event) { + const mainCheckbox = event.target; + + document + .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) + .forEach(checkbox => {checkbox.checked = mainCheckbox.checked;}); +} diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js new file mode 100644 index 000000000..b63c43928 --- /dev/null +++ b/bookwyrm/static/js/localstorage.js @@ -0,0 +1,17 @@ +// set javascript listeners +function updateDisplay(e) { + // used in set reading goal + var key = e.target.getAttribute('data-id'); + var value = e.target.getAttribute('data-value'); + window.localStorage.setItem(key, value); + + document.querySelectorAll('[data-hide="' + key + '"]') + .forEach(t => setDisplay(t)); +} + +function setDisplay(el) { + // used in set reading goal + var key = el.getAttribute('data-hide'); + var value = window.localStorage.getItem(key); + addRemoveClass(el, 'hidden', value); +} diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js index 65f0dd65b..d390f482f 100644 --- a/bookwyrm/static/js/shared.js +++ b/bookwyrm/static/js/shared.js @@ -8,29 +8,10 @@ window.onload = function() { Array.from(document.getElementsByClassName('interaction')) .forEach(t => t.onsubmit = interact); - // Toggle all checkboxes. - document - .querySelectorAll('[data-action="toggle-all"]') - .forEach(input => { - input.addEventListener('change', toggleAllCheckboxes); - }); - - // tab groups - Array.from(document.getElementsByClassName('tab-group')) - .forEach(t => new TabGroup(t)); - // handle aria settings on menus Array.from(document.getElementsByClassName('pulldown-menu')) .forEach(t => t.onclick = toggleMenu); - // display based on localstorage vars - document.querySelectorAll('[data-hide]') - .forEach(t => setDisplay(t)); - - // update localstorage - Array.from(document.getElementsByClassName('set-display')) - .forEach(t => t.onclick = updateDisplay); - // hidden submit button in a form document.querySelectorAll('.hidden-form input') .forEach(t => t.onchange = revealForm); @@ -42,6 +23,24 @@ window.onload = function() { // browser back behavior document.querySelectorAll('[data-back]') .forEach(t => t.onclick = back); + + Array.from(document.getElementsByClassName('tab-group')) + .forEach(t => new TabGroup(t)); + + // display based on localstorage vars + document.querySelectorAll('[data-hide]') + .forEach(t => setDisplay(t)); + + // update localstorage + Array.from(document.getElementsByClassName('set-display')) + .forEach(t => t.onclick = updateDisplay); + + // Toggle all checkboxes. + document + .querySelectorAll('[data-action="toggle-all"]') + .forEach(input => { + input.addEventListener('change', toggleAllCheckboxes); + }); }; function back(e) { @@ -78,24 +77,6 @@ function revealForm(e) { } -function updateDisplay(e) { - // used in set reading goal - var key = e.target.getAttribute('data-id'); - var value = e.target.getAttribute('data-value'); - window.localStorage.setItem(key, value); - - document.querySelectorAll('[data-hide="' + key + '"]') - .forEach(t => setDisplay(t)); -} - -function setDisplay(el) { - // used in set reading goal - var key = el.getAttribute('data-hide'); - var value = window.localStorage.getItem(key); - addRemoveClass(el, 'hidden', value); -} - - function toggleAction(e) { var el = e.currentTarget; var pressed = el.getAttribute('aria-pressed') == 'false'; @@ -139,22 +120,6 @@ function interact(e) { .forEach(t => addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1)); } -/** - * Toggle all descendant checkboxes of a target. - * - * Use `data-target="ID_OF_TARGET"` on the node being listened to. - * - * @param {Event} event - change Event - * @return {undefined} - */ -function toggleAllCheckboxes(event) { - const mainCheckbox = event.target; - - document - .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) - .forEach(checkbox => {checkbox.checked = mainCheckbox.checked;}); -} - function toggleMenu(e) { var el = e.currentTarget; var expanded = el.getAttribute('aria-expanded') == 'false'; @@ -200,258 +165,3 @@ function removeClass(el, className) { } el.className = classes.join(' '); } - -/* -* The content below is licensed according to the W3C Software License at -* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document -* Heavily modified to web component by Zach Leatherman -* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman -*/ -class TabGroup { - constructor(container) { - this.container = container; - - this.tablist = this.container.querySelector('[role="tablist"]'); - this.buttons = this.tablist.querySelectorAll('[role="tab"]'); - this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]'); - this.delay = this.determineDelay(); - - if(!this.tablist || !this.buttons.length || !this.panels.length) { - return; - } - - this.keys = this.keys(); - this.direction = this.direction(); - this.initButtons(); - this.initPanels(); - } - - keys() { - return { - end: 35, - home: 36, - left: 37, - up: 38, - right: 39, - down: 40 - }; - } - - // Add or substract depending on key pressed - direction() { - return { - 37: -1, - 38: -1, - 39: 1, - 40: 1 - }; - } - - initButtons() { - let count = 0; - for(let button of this.buttons) { - let isSelected = button.getAttribute("aria-selected") === "true"; - button.setAttribute("tabindex", isSelected ? "0" : "-1"); - - button.addEventListener('click', this.clickEventListener.bind(this)); - button.addEventListener('keydown', this.keydownEventListener.bind(this)); - button.addEventListener('keyup', this.keyupEventListener.bind(this)); - - button.index = count++; - } - } - - initPanels() { - let selectedPanelId = this.tablist.querySelector('[role="tab"][aria-selected="true"]').getAttribute("aria-controls"); - for(let panel of this.panels) { - if(panel.getAttribute("id") !== selectedPanelId) { - panel.setAttribute("hidden", ""); - } - panel.setAttribute("tabindex", "0"); - } - } - - clickEventListener(event) { - let button = event.target.closest('a'); - - event.preventDefault(); - - this.activateTab(button, false); - } - - // Handle keydown on tabs - keydownEventListener(event) { - var key = event.keyCode; - - switch (key) { - case this.keys.end: - event.preventDefault(); - // Activate last tab - this.activateTab(this.buttons[this.buttons.length - 1]); - break; - case this.keys.home: - event.preventDefault(); - // Activate first tab - this.activateTab(this.buttons[0]); - break; - - // Up and down are in keydown - // because we need to prevent page scroll >:) - case this.keys.up: - case this.keys.down: - this.determineOrientation(event); - break; - }; - } - - // Handle keyup on tabs - keyupEventListener(event) { - var key = event.keyCode; - - switch (key) { - case this.keys.left: - case this.keys.right: - this.determineOrientation(event); - break; - }; - } - - // When a tablist’s aria-orientation is set to vertical, - // only up and down arrow should function. - // In all other cases only left and right arrow function. - determineOrientation(event) { - var key = event.keyCode; - var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical'; - var proceed = false; - - if (vertical) { - if (key === this.keys.up || key === this.keys.down) { - event.preventDefault(); - proceed = true; - }; - } - else { - if (key === this.keys.left || key === this.keys.right) { - proceed = true; - }; - }; - - if (proceed) { - this.switchTabOnArrowPress(event); - }; - } - - // Either focus the next, previous, first, or last tab - // depending on key pressed - switchTabOnArrowPress(event) { - var pressed = event.keyCode; - - for (let button of this.buttons) { - button.addEventListener('focus', this.focusEventHandler.bind(this)); - }; - - if (this.direction[pressed]) { - var target = event.target; - if (target.index !== undefined) { - if (this.buttons[target.index + this.direction[pressed]]) { - this.buttons[target.index + this.direction[pressed]].focus(); - } - else if (pressed === this.keys.left || pressed === this.keys.up) { - this.focusLastTab(); - } - else if (pressed === this.keys.right || pressed == this.keys.down) { - this.focusFirstTab(); - } - } - } - } - - // Activates any given tab panel - activateTab (tab, setFocus) { - if(tab.getAttribute("role") !== "tab") { - tab = tab.closest('[role="tab"]'); - } - - setFocus = setFocus || true; - - // Deactivate all other tabs - this.deactivateTabs(); - - // Remove tabindex attribute - tab.removeAttribute('tabindex'); - - // Set the tab as selected - tab.setAttribute('aria-selected', 'true'); - - // Give the tab parent an is-active class - tab.parentNode.classList.add('is-active'); - - // Get the value of aria-controls (which is an ID) - var controls = tab.getAttribute('aria-controls'); - - // Remove hidden attribute from tab panel to make it visible - document.getElementById(controls).removeAttribute('hidden'); - - // Set focus when required - if (setFocus) { - tab.focus(); - } - } - - // Deactivate all tabs and tab panels - deactivateTabs() { - for (let button of this.buttons) { - button.parentNode.classList.remove('is-active'); - button.setAttribute('tabindex', '-1'); - button.setAttribute('aria-selected', 'false'); - button.removeEventListener('focus', this.focusEventHandler.bind(this)); - } - - for (let panel of this.panels) { - panel.setAttribute('hidden', 'hidden'); - } - } - - focusFirstTab() { - this.buttons[0].focus(); - } - - focusLastTab() { - this.buttons[this.buttons.length - 1].focus(); - } - - // Determine whether there should be a delay - // when user navigates with the arrow keys - determineDelay() { - var hasDelay = this.tablist.hasAttribute('data-delay'); - var delay = 0; - - if (hasDelay) { - var delayValue = this.tablist.getAttribute('data-delay'); - if (delayValue) { - delay = delayValue; - } - else { - // If no value is specified, default to 300ms - delay = 300; - }; - }; - - return delay; - } - - focusEventHandler(event) { - var target = event.target; - - setTimeout(this.checkTabFocus.bind(this), this.delay, target); - }; - - // Only activate tab on focus if it still has focus after the delay - checkTabFocus(target) { - let focused = document.activeElement; - - if (target === focused) { - this.activateTab(target, false); - } - } - } diff --git a/bookwyrm/static/js/tabs.js b/bookwyrm/static/js/tabs.js new file mode 100644 index 000000000..1cb525ce9 --- /dev/null +++ b/bookwyrm/static/js/tabs.js @@ -0,0 +1,254 @@ +/* +* The content below is licensed according to the W3C Software License at +* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document +* Heavily modified to web component by Zach Leatherman +* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman +*/ +class TabGroup { + constructor(container) { + this.container = container; + + this.tablist = this.container.querySelector('[role="tablist"]'); + this.buttons = this.tablist.querySelectorAll('[role="tab"]'); + this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]'); + this.delay = this.determineDelay(); + + if(!this.tablist || !this.buttons.length || !this.panels.length) { + return; + } + + this.keys = this.keys(); + this.direction = this.direction(); + this.initButtons(); + this.initPanels(); + } + + keys() { + return { + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40 + }; + } + + // Add or substract depending on key pressed + direction() { + return { + 37: -1, + 38: -1, + 39: 1, + 40: 1 + }; + } + + initButtons() { + let count = 0; + for(let button of this.buttons) { + let isSelected = button.getAttribute("aria-selected") === "true"; + button.setAttribute("tabindex", isSelected ? "0" : "-1"); + + button.addEventListener('click', this.clickEventListener.bind(this)); + button.addEventListener('keydown', this.keydownEventListener.bind(this)); + button.addEventListener('keyup', this.keyupEventListener.bind(this)); + + button.index = count++; + } + } + + initPanels() { + let selectedPanelId = this.tablist.querySelector('[role="tab"][aria-selected="true"]').getAttribute("aria-controls"); + for(let panel of this.panels) { + if(panel.getAttribute("id") !== selectedPanelId) { + panel.setAttribute("hidden", ""); + } + panel.setAttribute("tabindex", "0"); + } + } + + clickEventListener(event) { + let button = event.target.closest('a'); + + event.preventDefault(); + + this.activateTab(button, false); + } + + // Handle keydown on tabs + keydownEventListener(event) { + var key = event.keyCode; + + switch (key) { + case this.keys.end: + event.preventDefault(); + // Activate last tab + this.activateTab(this.buttons[this.buttons.length - 1]); + break; + case this.keys.home: + event.preventDefault(); + // Activate first tab + this.activateTab(this.buttons[0]); + break; + + // Up and down are in keydown + // because we need to prevent page scroll >:) + case this.keys.up: + case this.keys.down: + this.determineOrientation(event); + break; + } + } + + // Handle keyup on tabs + keyupEventListener(event) { + var key = event.keyCode; + + switch (key) { + case this.keys.left: + case this.keys.right: + this.determineOrientation(event); + break; + } + } + + // When a tablist’s aria-orientation is set to vertical, + // only up and down arrow should function. + // In all other cases only left and right arrow function. + determineOrientation(event) { + var key = event.keyCode; + var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical'; + var proceed = false; + + if (vertical) { + if (key === this.keys.up || key === this.keys.down) { + event.preventDefault(); + proceed = true; + } + } + else { + if (key === this.keys.left || key === this.keys.right) { + proceed = true; + } + } + + if (proceed) { + this.switchTabOnArrowPress(event); + } + } + + // Either focus the next, previous, first, or last tab + // depending on key pressed + switchTabOnArrowPress(event) { + var pressed = event.keyCode; + + for (let button of this.buttons) { + button.addEventListener('focus', this.focusEventHandler.bind(this)); + } + + if (this.direction[pressed]) { + var target = event.target; + if (target.index !== undefined) { + if (this.buttons[target.index + this.direction[pressed]]) { + this.buttons[target.index + this.direction[pressed]].focus(); + } + else if (pressed === this.keys.left || pressed === this.keys.up) { + this.focusLastTab(); + } + else if (pressed === this.keys.right || pressed == this.keys.down) { + this.focusFirstTab(); + } + } + } + } + + // Activates any given tab panel + activateTab (tab, setFocus) { + if(tab.getAttribute("role") !== "tab") { + tab = tab.closest('[role="tab"]'); + } + + setFocus = setFocus || true; + + // Deactivate all other tabs + this.deactivateTabs(); + + // Remove tabindex attribute + tab.removeAttribute('tabindex'); + + // Set the tab as selected + tab.setAttribute('aria-selected', 'true'); + + // Give the tab parent an is-active class + tab.parentNode.classList.add('is-active'); + + // Get the value of aria-controls (which is an ID) + var controls = tab.getAttribute('aria-controls'); + + // Remove hidden attribute from tab panel to make it visible + document.getElementById(controls).removeAttribute('hidden'); + + // Set focus when required + if (setFocus) { + tab.focus(); + } + } + + // Deactivate all tabs and tab panels + deactivateTabs() { + for (let button of this.buttons) { + button.parentNode.classList.remove('is-active'); + button.setAttribute('tabindex', '-1'); + button.setAttribute('aria-selected', 'false'); + button.removeEventListener('focus', this.focusEventHandler.bind(this)); + } + + for (let panel of this.panels) { + panel.setAttribute('hidden', 'hidden'); + } + } + + focusFirstTab() { + this.buttons[0].focus(); + } + + focusLastTab() { + this.buttons[this.buttons.length - 1].focus(); + } + + // Determine whether there should be a delay + // when user navigates with the arrow keys + determineDelay() { + var hasDelay = this.tablist.hasAttribute('data-delay'); + var delay = 0; + + if (hasDelay) { + var delayValue = this.tablist.getAttribute('data-delay'); + if (delayValue) { + delay = delayValue; + } + else { + // If no value is specified, default to 300ms + delay = 300; + } + } + + return delay; + } + + focusEventHandler(event) { + var target = event.target; + + setTimeout(this.checkTabFocus.bind(this), this.delay, target); + } + + // Only activate tab on focus if it still has focus after the delay + checkTabFocus(target) { + let focused = document.activeElement; + + if (target === focused) { + this.activateTab(target, false); + } + } +} diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 4dc4991d0..7f0757410 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -1,4 +1,4 @@ -''' Handle user activity ''' +""" Handle user activity """ from django.db import transaction from django.utils import timezone @@ -7,14 +7,14 @@ from bookwyrm.sanitize_html import InputHtmlParser def delete_status(status): - ''' replace the status with a tombstone ''' + """ replace the status with a tombstone """ status.deleted = True status.deleted_date = timezone.now() status.save() -def create_generated_note(user, content, mention_books=None, privacy='public'): - ''' a note created by the app about user activity ''' +def create_generated_note(user, content, mention_books=None, privacy="public"): + """ a note created by the app about user activity """ # sanitize input html parser = InputHtmlParser() parser.feed(content) @@ -22,11 +22,7 @@ def create_generated_note(user, content, mention_books=None, privacy='public'): with transaction.atomic(): # create but don't save - status = models.GeneratedNote( - user=user, - content=content, - privacy=privacy - ) + status = models.GeneratedNote(user=user, content=content, privacy=privacy) # we have to save it to set the related fields, but hold off on telling # folks about it because it is not ready status.save(broadcast=False) diff --git a/bookwyrm/tasks.py b/bookwyrm/tasks.py index fc0b9739b..23765f09b 100644 --- a/bookwyrm/tasks.py +++ b/bookwyrm/tasks.py @@ -1,12 +1,12 @@ -''' background tasks ''' +""" background tasks """ import os from celery import Celery from bookwyrm import settings # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'celerywyrm.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings") app = Celery( - 'tasks', + "tasks", broker=settings.CELERY_BROKER, ) diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book/book.html similarity index 80% rename from bookwyrm/templates/book.html rename to bookwyrm/templates/book/book.html index 16bf11972..0d908110b 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book/book.html @@ -35,42 +35,37 @@
-
+
{% include 'snippets/book_cover.html' with book=book size=large %} {% include 'snippets/rate_action.html' with user=request.user book=book %} {% include 'snippets/shelve_button/shelve_button.html' %} {% if request.user.is_authenticated and not book.cover %} -
-

{% trans "Add cover" %}

-
- {% csrf_token %} - - -
+
+ {% trans "Add cover" as button_text %} + {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="add-cover" controls_uid=book.id focus="modal-title-add-cover" class="is-small" %} + {% include 'book/cover_modal.html' with book=book controls_text="add-cover" controls_uid=book.id %}
{% endif %}
{% if book.isbn_13 %} -
+
{% trans "ISBN:" %}
{{ book.isbn_13 }}
{% endif %} {% if book.oclc_number %} -
+
{% trans "OCLC Number:" %}
{{ book.oclc_number }}
{% endif %} {% if book.asin %} -
+
{% trans "ASIN:" %}
{{ book.asin }}
@@ -78,13 +73,22 @@

- {% if book.physical_format and not book.pages %} - {{ book.physical_format | title }} - {% elif book.physical_format and book.pages %} - {% blocktrans with format=book.physical_format|title pages=book.pages %}{{ format }}, {{ pages }} pages{% endblocktrans %} - {% elif book.pages %} - {% blocktrans with pages=book.pages %}{{ pages }} pages{% endblocktrans %} - {% endif %} + {% if book.physical_format and not book.pages %} + {{ book.physical_format | title }} + {% elif book.physical_format and book.pages %} + {% blocktrans with format=book.physical_format|title pages=book.pages %}{{ format }}, {{ pages }} pages{% endblocktrans %} + {% elif book.pages %} + {% blocktrans with pages=book.pages %}{{ pages }} pages{% endblocktrans %} + {% endif %} +

+

+ {% if book.published_date and book.publishers %} + {% blocktrans with date=book.published_date|date:'M jS Y' publisher=book.publishers|join:', ' %}Published {{ date }} by {{ publisher }}.{% endblocktrans %} + {% elif book.published_date %} + {% blocktrans with date=book.published_date|date:'M jS Y' %}Published {{ date }}{% endblocktrans %} + {% elif book.publishers %} + {% blocktrans with publisher=book.publishers|join:', ' %}Published by {{ publisher }}.{% endblocktrans %} + {% endif %}

{% if book.openlibrary_key %} @@ -224,7 +228,7 @@
{% endif %} - {% if lists.exists %} + {% if lists.exists or request.user.list_set.exists %}

{% trans "Lists" %}

+ + {% if request.user.list_set.exists %} +
+ {% csrf_token %} + + +
+
+ +
+
+ +
+
+
+ {% endif %}
{% endif %}
diff --git a/bookwyrm/templates/book/cover_modal.html b/bookwyrm/templates/book/cover_modal.html new file mode 100644 index 000000000..f09b44951 --- /dev/null +++ b/bookwyrm/templates/book/cover_modal.html @@ -0,0 +1,36 @@ +{% extends 'components/modal.html' %} +{% load i18n %} + +{% block modal-title %} +{% trans "Add cover" %} +{% endblock %} + +{% block modal-form-open %} +
+{% endblock %} + +{% block modal-body %} + +{% endblock %} + +{% block modal-footer %} + +{% trans "Cancel" as button_text %} +{% include 'snippets/toggle/toggle_button.html' with text=button_text %} +{% endblock %} +{% block modal-form-close %}
{% endblock %} + diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html new file mode 100644 index 000000000..d7c842351 --- /dev/null +++ b/bookwyrm/templates/book/edit_book.html @@ -0,0 +1,231 @@ +{% extends 'layout.html' %} +{% load i18n %} +{% load humanize %} + +{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %} + +{% block content %} +
+

+ {% if book %} + {% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %} + {% else %} + {% trans "Add Book" %} + {% endif %} +

+ {% if book %} +
+

{% trans "Added:" %} {{ book.created_date | naturaltime }}

+

{% trans "Updated:" %} {{ book.updated_date | naturaltime }}

+

{% trans "Last edited by:" %} {{ book.last_edited_by.display_name }}

+
+ {% endif %} +
+ +{% if form.non_field_errors %} +
+

{{ form.non_field_errors }}

+
+{% endif %} + +{% if book %} +
+{% else %} + +{% endif %} + + {% csrf_token %} + {% if confirm_mode %} +
+

{% trans "Confirm Book Info" %}

+
+ {% if author_matches %} + +
+ {% for author in author_matches %} +
+ {% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %} + {% with forloop.counter0 as counter %} + {% for match in author.matches %} + +

+ {% blocktrans with book_title=match.book_set.first.title %}Author of {{ book_title }}{% endblocktrans %} +

+ {% endfor %} + + {% endwith %} +
+ {% endfor %} +
+ {% else %} +

{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}

+ {% endif %} + + {% if not book %} +
+
+ {% trans "Is this an edition of an existing work?" %} + {% for match in book_matches %} + + {% endfor %} + +
+
+ {% endif %} +
+ + + + {% trans "Back" %} + +
+ +
+ {% endif %} + + +
+
+
+

{% trans "Metadata" %}

+

{{ form.title }}

+ {% for error in form.title.errors %} +

{{ error | escape }}

+ {% endfor %} + +

{{ form.subtitle }}

+ {% for error in form.subtitle.errors %} +

{{ error | escape }}

+ {% endfor %} + +

{{ form.description }}

+ {% for error in form.description.errors %} +

{{ error | escape }}

+ {% endfor %} + +

{{ form.series }}

+ {% for error in form.series.errors %} +

{{ error | escape }}

+ {% endfor %} + +

{{ form.series_number }}

+ {% for error in form.series_number.errors %} +

{{ error | escape }}

+ {% endfor %} + +

+ + {{ form.publishers }} + {% trans "Separate multiple publishers with commas." %} +

+ {% for error in form.publishers.errors %} +

{{ error | escape }}

+ {% endfor %} + +

{{ form.first_published_date }}

+ {% for error in form.first_published_date.errors %} +

{{ error | escape }}

+ {% endfor %} + +

{{ form.published_date }}

+ {% for error in form.published_date.errors %} +

{{ error | escape }}

+ {% endfor %} +
+ +
+

{% trans "Authors" %}

+ {% if book.authors.exists %} +
+ {% for author in book.authors.all %} + + {% endfor %} +
+ {% endif %} + + +

Separate multiple author names with commas.

+
+
+ +
+

{% trans "Cover" %}

+
+
+ {% include 'snippets/book_cover.html' with book=book size="small" %} +
+
+
+

+ + {{ form.cover }} +

+ {% if book %} +

+ + +

+ {% endif %} + {% for error in form.cover.errors %} +

{{ error | escape }}

+ {% endfor %} +
+
+
+ +
+

{% trans "Physical Properties" %}

+

{{ form.physical_format }}

+ {% for error in form.physical_format.errors %} +

{{ error | escape }}

+ {% endfor %} + {% for error in form.physical_format.errors %} +

{{ error | escape }}

+ {% endfor %} + +

{{ form.pages }}

+ {% for error in form.pages.errors %} +

{{ error | escape }}

+ {% endfor %} +
+ +
+

{% trans "Book Identifiers" %}

+

{{ form.isbn_13 }}

+ {% for error in form.isbn_13.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.isbn_10 }}

+ {% for error in form.isbn_10.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.openlibrary_key }}

+ {% for error in form.openlibrary_key.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.oclc_number }}

+ {% for error in form.oclc_number.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.asin }}

+ {% for error in form.ASIN.errors %} +

{{ error | escape }}

+ {% endfor %} +
+
+
+ + {% if not confirm_mode %} +
+ + {% trans "Cancel" %} +
+ {% endif %} +
+ +{% endblock %} diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html deleted file mode 100644 index 4d2159497..000000000 --- a/bookwyrm/templates/edit_book.html +++ /dev/null @@ -1,125 +0,0 @@ -{% extends 'layout.html' %} -{% load i18n %} -{% load humanize %} - -{% block title %}{% trans "Edit Book" %}{% endblock %} - -{% block content %} -
-

- Edit "{{ book.title }}" -

-
-

{% trans "Added:" %} {{ book.created_date | naturaltime }}

-

{% trans "Updated:" %} {{ book.updated_date | naturaltime }}

-

{% trans "Last edited by:" %} {{ book.last_edited_by.display_name }}

-
-
- -{% if form.non_field_errors %} -
-

{{ form.non_field_errors }}

-
-{% endif %} - -
- {% csrf_token %} - -
-
-

{% trans "Metadata" %}

-

{{ form.title }}

- {% for error in form.title.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.subtitle }}

- {% for error in form.subtitle.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.description }}

- {% for error in form.description.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.series }}

- {% for error in form.series.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.series_number }}

- {% for error in form.series_number.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.first_published_date }}

- {% for error in form.first_published_date.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.published_date }}

- {% for error in form.published_date.errors %} -

{{ error | escape }}

- {% endfor %} -
- -
-
-
- {% include 'snippets/book_cover.html' with book=book size="small" %} -
-
-
-

{% trans "Cover" %}

-

{{ form.cover }}

- {% for error in form.cover.errors %} -

{{ error | escape }}

- {% endfor %} -
-
-
- -
-

{% trans "Physical Properties" %}

-

{{ form.physical_format }}

- {% for error in form.physical_format.errors %} -

{{ error | escape }}

- {% endfor %} - {% for error in form.physical_format.errors %} -

{{ error | escape }}

- {% endfor %} - -

{{ form.pages }}

- {% for error in form.pages.errors %} -

{{ error | escape }}

- {% endfor %} -
- -
-

{% trans "Book Identifiers" %}

-

{{ form.isbn_13 }}

- {% for error in form.isbn_13.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.isbn_10 }}

- {% for error in form.isbn_10.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.openlibrary_key }}

- {% for error in form.openlibrary_key.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.oclc_number }}

- {% for error in form.oclc_number.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.asin }}

- {% for error in form.ASIN.errors %} -

{{ error | escape }}

- {% endfor %} -
-
-
- -
- - {% trans "Cancel" %} -
-
- -{% endblock %} diff --git a/bookwyrm/templates/editions.html b/bookwyrm/templates/editions.html index 38147a86e..f83197579 100644 --- a/bookwyrm/templates/editions.html +++ b/bookwyrm/templates/editions.html @@ -6,7 +6,7 @@ {% block content %}
-

{% blocktrans with path=work.local_path work_title=work.title %}Editions of "{{ work_title }}"{% endblocktrans %}

+

{% blocktrans with work_path=work.local_path work_title=work.title %}Editions of "{{ work_title }}"{% endblocktrans %}

{% include 'snippets/book_tiles.html' with books=editions %}
diff --git a/bookwyrm/templates/feed/feed.html b/bookwyrm/templates/feed/feed.html index 4eb363e4a..b7ff6e253 100644 --- a/bookwyrm/templates/feed/feed.html +++ b/bookwyrm/templates/feed/feed.html @@ -19,9 +19,9 @@
{# announcements and system messages #} -{% if not goal and tab == 'home' %} +{% if request.user.show_goal and not goal and tab == 'home' %} {% now 'Y' as year %} -
{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/bookwyrm/templates/import_status.html b/bookwyrm/templates/import_status.html index f9ba36bf4..c2985c12a 100644 --- a/bookwyrm/templates/import_status.html +++ b/bookwyrm/templates/import_status.html @@ -148,3 +148,7 @@
{% endspaceless %}{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 377acb6c5..c7d10b573 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -79,7 +79,7 @@ aria-controls="navbar-dropdown" > {% include 'snippets/avatar.html' with user=request.user %} - {{ user.display_name }} + {{ request.user.display_name }} {% endif %} -
+
{% block panel %}{% endblock %}
diff --git a/bookwyrm/templates/settings/site.html b/bookwyrm/templates/settings/site.html index 2e43bb74d..27be0c9b2 100644 --- a/bookwyrm/templates/settings/site.html +++ b/bookwyrm/templates/settings/site.html @@ -7,7 +7,7 @@ {% block panel %} -
+ {% csrf_token %}

{% trans "Instance Info" %}

diff --git a/bookwyrm/templates/snippets/about.html b/bookwyrm/templates/snippets/about.html index aad5d5dee..820fb5759 100644 --- a/bookwyrm/templates/snippets/about.html +++ b/bookwyrm/templates/snippets/about.html @@ -1,7 +1,7 @@
- BookWyrm logo + BookWyrm logo
diff --git a/bookwyrm/templates/snippets/follow_button.html b/bookwyrm/templates/snippets/follow_button.html index 419aa211b..3df85a1a6 100644 --- a/bookwyrm/templates/snippets/follow_button.html +++ b/bookwyrm/templates/snippets/follow_button.html @@ -1,18 +1,12 @@ {% load i18n %} {% if request.user == user or not request.user.is_authenticated %} -{% elif request.user in user.follower_requests.all %} - -
- {% trans "Follow request already sent." %} -
- {% elif user in request.user.blocks.all %} {% include 'snippets/block_button.html' %} {% else %}
- + {% csrf_token %} {% if user.manually_approves_followers %} @@ -21,10 +15,14 @@ {% endif %} -
diff --git a/bookwyrm/templates/snippets/generated_status/rating.html b/bookwyrm/templates/snippets/generated_status/rating.html new file mode 100644 index 000000000..13afd94de --- /dev/null +++ b/bookwyrm/templates/snippets/generated_status/rating.html @@ -0,0 +1,3 @@ +{% load i18n %}{% load humanize %} + +{% blocktrans with title=book.title path=book.remote_id rating=rating count counter=rating %}Rated {{ title }}: {{ rating }} star{% plural %}Rated {{ title }}: {{ rating }} stars{% endblocktrans %} diff --git a/bookwyrm/templates/snippets/goal_card.html b/bookwyrm/templates/snippets/goal_card.html index 084a5ad0b..329fea542 100644 --- a/bookwyrm/templates/snippets/goal_card.html +++ b/bookwyrm/templates/snippets/goal_card.html @@ -17,8 +17,9 @@ {% endblock %} {% block card-footer %} - + {% endblock %} diff --git a/bookwyrm/templates/snippets/progress_update.html b/bookwyrm/templates/snippets/progress_update.html index 138defbd7..cb7096258 100644 --- a/bookwyrm/templates/snippets/progress_update.html +++ b/bookwyrm/templates/snippets/progress_update.html @@ -22,7 +22,7 @@
{% if readthrough.progress_mode == 'PG' and book.pages %} -

{% blocktrans %}of {{ book.pages }} pages{% endblocktrans %}

+

{% blocktrans with pages=book.pages %}of {{ pages }} pages{% endblocktrans %}

{% endif %}
diff --git a/bookwyrm/templates/snippets/report_button.html b/bookwyrm/templates/snippets/report_button.html new file mode 100644 index 000000000..2fa0a3f30 --- /dev/null +++ b/bookwyrm/templates/snippets/report_button.html @@ -0,0 +1,10 @@ +{% load i18n %} +{% load bookwyrm_tags %} +{% with 0|uuid as report_uuid %} + +{% trans "Report" as button_text %} +{% include 'snippets/toggle/toggle_button.html' with class="is-danger is-light is-small is-fullwidth" text=button_text controls_text="report" controls_uid=report_uuid focus="modal-title-report" disabled=is_current %} + +{% include 'moderation/report_modal.html' with user=user reporter=request.user controls_text="report" controls_uid=report_uuid %} + +{% endwith %} diff --git a/bookwyrm/templates/snippets/search_result_text.html b/bookwyrm/templates/snippets/search_result_text.html index 360090509..059b8e7e8 100644 --- a/bookwyrm/templates/snippets/search_result_text.html +++ b/bookwyrm/templates/snippets/search_result_text.html @@ -1,3 +1,34 @@ {% load i18n %} -{% if link %}{{ result.title }}{% else %}{{ result.title }}{% endif %} -{% if result.author %} {% blocktrans with author=result.author %}by {{ author }}{% endblocktrans %}{% endif %}{% if result.year %} ({{ result.year }}){% endif %} +
+
+ {% if result.cover %} + + {% else %} +
+ +
+

{% trans "No cover" %}

+
+
+ {% endif %} +
+ +
+

+ + {{ result.title }} + + {% if result.author %} + {% blocktrans with author=result.author %}by {{ author }}{% endblocktrans %}{% endif %}{% if result.year %} ({{ result.year }}) + {% endif %} +

+ + {% if remote_result %} +
+ {% csrf_token %} + + +
+ {% endif %} +
+
diff --git a/bookwyrm/templates/snippets/status/status.html b/bookwyrm/templates/snippets/status/status.html index 50d3414dd..10492c48b 100644 --- a/bookwyrm/templates/snippets/status/status.html +++ b/bookwyrm/templates/snippets/status/status.html @@ -4,7 +4,7 @@ {% if status.status_type == 'Announce' %} {% include 'snippets/avatar.html' with user=status.user %} - {{ user.display_name }} + {{ status.user.display_name }} {% trans "boosted" %} {% include 'snippets/status/status_body.html' with status=status|boosted_status %} diff --git a/bookwyrm/templates/snippets/status/status_body.html b/bookwyrm/templates/snippets/status/status_body.html index 8d6c21ed9..a7e8e8843 100644 --- a/bookwyrm/templates/snippets/status/status_body.html +++ b/bookwyrm/templates/snippets/status/status_body.html @@ -18,7 +18,17 @@ {% block card-footer %}